From f0d71ba35646d6b54b05943a18ed9afea7781760 Mon Sep 17 00:00:00 2001 From: Koen Vlaswinkel Date: Fri, 7 Oct 2022 14:30:44 +0200 Subject: [PATCH 1/8] Use real `CancellationTokenSource` in tests This will change tests that are using a mocked `CancellationTokenSource` to use a real `CancellationTokenSource` instead. Tests are run inside VSCode, so we can use these without mocking. --- .../remote-queries/run-remote-query.test.ts | 13 +++---------- .../remote-queries/variant-analysis-manager.test.ts | 11 ++--------- .../remote-queries/variant-analysis-monitor.test.ts | 11 ++--------- 3 files changed, 7 insertions(+), 28 deletions(-) diff --git a/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/run-remote-query.test.ts b/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/run-remote-query.test.ts index 187169e6a..49a3cbbcb 100644 --- a/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/run-remote-query.test.ts +++ b/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/run-remote-query.test.ts @@ -56,14 +56,7 @@ describe('Remote queries', function() { } credentials = {} as unknown as Credentials; - cancellationTokenSource = { - token: { - isCancellationRequested: false, - onCancellationRequested: sandbox.stub() - }, - cancel: sandbox.stub(), - dispose: sandbox.stub() - }; + cancellationTokenSource = new CancellationTokenSource(); progress = sandbox.spy(); // Should not have asked for a language @@ -282,7 +275,7 @@ describe('Remote queries', function() { const promise = runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token); - cancellationTokenSource.token.isCancellationRequested = true; + cancellationTokenSource.cancel(); try { await promise; @@ -347,7 +340,7 @@ describe('Remote queries', function() { const promise = runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token); - cancellationTokenSource.token.isCancellationRequested = true; + cancellationTokenSource.cancel(); try { await promise; diff --git a/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-manager.test.ts b/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-manager.test.ts index af4a7f157..6be98575a 100644 --- a/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-manager.test.ts +++ b/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-manager.test.ts @@ -36,14 +36,7 @@ describe('Variant Analysis Manager', async function() { sandbox.stub(fs, 'mkdirSync'); sandbox.stub(fs, 'writeFile'); - cancellationTokenSource = { - token: { - isCancellationRequested: false, - onCancellationRequested: sandbox.stub() - }, - cancel: sandbox.stub(), - dispose: sandbox.stub() - }; + cancellationTokenSource = new CancellationTokenSource(); scannedRepos = createMockScannedRepos(); variantAnalysis = createMockApiResponse('in_progress', scannedRepos); @@ -120,7 +113,7 @@ describe('Variant Analysis Manager', async function() { }); it('should return early if variant analysis is cancelled', async () => { - cancellationTokenSource.token.isCancellationRequested = true; + cancellationTokenSource.cancel(); await variantAnalysisManager.autoDownloadVariantAnalysisResult( scannedRepos[0], diff --git a/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-monitor.test.ts b/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-monitor.test.ts index 7ce204690..d38911875 100644 --- a/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-monitor.test.ts +++ b/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-monitor.test.ts @@ -31,14 +31,7 @@ describe('Variant Analysis Monitor', async function() { sandbox.stub(logger, 'log'); sandbox.stub(config, 'isVariantAnalysisLiveResultsEnabled').returns(false); - cancellationTokenSource = { - token: { - isCancellationRequested: false, - onCancellationRequested: sandbox.stub() - }, - cancel: sandbox.stub(), - dispose: sandbox.stub() - }; + cancellationTokenSource = new CancellationTokenSource(); variantAnalysis = createMockVariantAnalysis(); @@ -79,7 +72,7 @@ describe('Variant Analysis Monitor', async function() { }); it('should return early if variant analysis is cancelled', async () => { - cancellationTokenSource.token.isCancellationRequested = true; + cancellationTokenSource.cancel(); const result = await variantAnalysisMonitor.monitorVariantAnalysis(variantAnalysis, cancellationTokenSource.token); From c26d786a1cac98ba4f6bf22e65133fdbba60e38d Mon Sep 17 00:00:00 2001 From: Elena Tanasoiu Date: Tue, 4 Oct 2022 10:55:33 +0100 Subject: [PATCH 2/8] Emit variantAnalysisAdded event When we first submit the variant analysis for processing, we'd like to update the query history panel. At the moment we're just adding the setup for triggering the event. In a future PR we'll consume this event and change the query history panel accordingly. In order for this to happen we will need to introduce a new `VariantAnalysisHistoryItem` type which will massage the data we get from the API into a type which the Query History panel can consume. Co-authored-by: Shati Patel --- extensions/ql-vscode/src/extension.ts | 8 +++--- .../remote-queries/remote-queries-manager.ts | 7 ++++- .../src/remote-queries/run-remote-query.ts | 6 +++- .../variant-analysis-manager.ts | 9 +++++- .../remote-queries/run-remote-query.test.ts | 28 +++++++++++++------ 5 files changed, 42 insertions(+), 16 deletions(-) diff --git a/extensions/ql-vscode/src/extension.ts b/extensions/ql-vscode/src/extension.ts index 39c26e2e6..c5d16765a 100644 --- a/extensions/ql-vscode/src/extension.ts +++ b/extensions/ql-vscode/src/extension.ts @@ -467,16 +467,16 @@ async function activateWithInstalledDistribution( const localQueryResultsView = new ResultsView(ctx, dbm, cliServer, queryServerLogger, labelProvider); ctx.subscriptions.push(localQueryResultsView); - void logger.log('Initializing remote queries manager.'); - const rqm = new RemoteQueriesManager(ctx, cliServer, queryStorageDir, logger); - ctx.subscriptions.push(rqm); - void logger.log('Initializing variant analysis manager.'); const variantAnalysisStorageDir = path.join(ctx.globalStorageUri.fsPath, 'variant-analyses'); await fs.ensureDir(variantAnalysisStorageDir); const variantAnalysisManager = new VariantAnalysisManager(ctx, cliServer, variantAnalysisStorageDir, logger); ctx.subscriptions.push(variantAnalysisManager); + void logger.log('Initializing remote queries manager.'); + const rqm = new RemoteQueriesManager(ctx, cliServer, queryStorageDir, logger, variantAnalysisManager); + ctx.subscriptions.push(rqm); + void logger.log('Initializing query history.'); const qhm = new QueryHistoryManager( qs, diff --git a/extensions/ql-vscode/src/remote-queries/remote-queries-manager.ts b/extensions/ql-vscode/src/remote-queries/remote-queries-manager.ts index 71319ecba..6350c0b5f 100644 --- a/extensions/ql-vscode/src/remote-queries/remote-queries-manager.ts +++ b/extensions/ql-vscode/src/remote-queries/remote-queries-manager.ts @@ -22,6 +22,7 @@ import { assertNever } from '../pure/helpers-pure'; import { QueryStatus } from '../query-status'; import { DisposableObject } from '../pure/disposable-object'; import { AnalysisResults } from './shared/analysis-result'; +import { VariantAnalysisManager } from './variant-analysis-manager'; const autoDownloadMaxSize = 300 * 1024; const autoDownloadMaxCount = 100; @@ -56,6 +57,7 @@ export class RemoteQueriesManager extends DisposableObject { private readonly remoteQueriesMonitor: RemoteQueriesMonitor; private readonly analysesResultsManager: AnalysesResultsManager; + private readonly variantAnalysisManager: VariantAnalysisManager; private readonly view: RemoteQueriesView; constructor( @@ -63,11 +65,13 @@ export class RemoteQueriesManager extends DisposableObject { private readonly cliServer: CodeQLCliServer, private readonly storagePath: string, logger: Logger, + variantAnalysisManager: VariantAnalysisManager, ) { super(); this.analysesResultsManager = new AnalysesResultsManager(ctx, cliServer, storagePath, logger); this.view = new RemoteQueriesView(ctx, logger, this.analysesResultsManager); this.remoteQueriesMonitor = new RemoteQueriesMonitor(ctx, logger); + this.variantAnalysisManager = variantAnalysisManager; this.remoteQueryAddedEventEmitter = this.push(new EventEmitter()); this.remoteQueryRemovedEventEmitter = this.push(new EventEmitter()); @@ -123,7 +127,8 @@ export class RemoteQueriesManager extends DisposableObject { credentials, uri || window.activeTextEditor?.document.uri, false, progress, - token); + token, + this.variantAnalysisManager); if (querySubmission?.query) { const query = querySubmission.query; diff --git a/extensions/ql-vscode/src/remote-queries/run-remote-query.ts b/extensions/ql-vscode/src/remote-queries/run-remote-query.ts index d6505ac67..bc1a4e41b 100644 --- a/extensions/ql-vscode/src/remote-queries/run-remote-query.ts +++ b/extensions/ql-vscode/src/remote-queries/run-remote-query.ts @@ -29,6 +29,7 @@ import { getRepositorySelection, isValidSelection, RepositorySelection } from '. import { parseVariantAnalysisQueryLanguage, VariantAnalysisSubmission } from './shared/variant-analysis'; import { Repository } from './shared/repository'; import { processVariantAnalysis } from './variant-analysis-processor'; +import { VariantAnalysisManager } from './variant-analysis-manager'; export interface QlPack { name: string; @@ -182,7 +183,8 @@ export async function runRemoteQuery( uri: Uri | undefined, dryRun: boolean, progress: ProgressCallback, - token: CancellationToken + token: CancellationToken, + variantAnalysisManager: VariantAnalysisManager ): Promise { if (!(await cliServer.cliConstraints.supportsRemoteQueries())) { throw new Error(`Variant analysis is not supported by this version of CodeQL. Please upgrade to v${cli.CliVersionConstraint.CLI_VERSION_REMOTE_QUERIES @@ -273,6 +275,8 @@ export async function runRemoteQuery( const processedVariantAnalysis = processVariantAnalysis(variantAnalysisSubmission, variantAnalysisResponse); + variantAnalysisManager.onVariantAnalysisSubmitted(processedVariantAnalysis); + void logger.log(`Variant analysis:\n${JSON.stringify(processedVariantAnalysis, null, 2)}`); void showAndLogInformationMessage(`Variant analysis ${processedVariantAnalysis.query.name} submitted for processing`); diff --git a/extensions/ql-vscode/src/remote-queries/variant-analysis-manager.ts b/extensions/ql-vscode/src/remote-queries/variant-analysis-manager.ts index 424ffcca7..50805f79c 100644 --- a/extensions/ql-vscode/src/remote-queries/variant-analysis-manager.ts +++ b/extensions/ql-vscode/src/remote-queries/variant-analysis-manager.ts @@ -1,5 +1,5 @@ import * as ghApiClient from './gh-api/gh-api-client'; -import { CancellationToken, ExtensionContext } from 'vscode'; +import { CancellationToken, EventEmitter, ExtensionContext } from 'vscode'; import { DisposableObject } from '../pure/disposable-object'; import { Logger } from '../logging'; import { Credentials } from '../authentication'; @@ -21,6 +21,9 @@ import { VariantAnalysisResultsManager } from './variant-analysis-results-manage import { CodeQLCliServer } from '../cli'; export class VariantAnalysisManager extends DisposableObject implements VariantAnalysisViewManager { + private readonly _onVariantAnalysisAdded = this.push(new EventEmitter()); + readonly onVariantAnalysisAdded = this._onVariantAnalysisAdded.event; + private readonly variantAnalysisMonitor: VariantAnalysisMonitor; private readonly variantAnalysisResultsManager: VariantAnalysisResultsManager; private readonly views = new Map(); @@ -73,6 +76,10 @@ export class VariantAnalysisManager extends DisposableObject implements VariantA await this.getView(variantAnalysis.id)?.updateView(variantAnalysis); } + public onVariantAnalysisSubmitted(variantAnalysis: VariantAnalysis): void { + this._onVariantAnalysisAdded.fire(variantAnalysis); + } + private async onRepoStateUpdated(variantAnalysisId: number, repoState: VariantAnalysisScannedRepositoryState): Promise { await this.getView(variantAnalysisId)?.updateRepoState(repoState); } diff --git a/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/run-remote-query.test.ts b/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/run-remote-query.test.ts index 187169e6a..5a16c9913 100644 --- a/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/run-remote-query.test.ts +++ b/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/run-remote-query.test.ts @@ -1,7 +1,7 @@ import { assert, expect } from 'chai'; import * as path from 'path'; import * as sinon from 'sinon'; -import { CancellationTokenSource, extensions, QuickPickItem, Uri, window } from 'vscode'; +import { CancellationTokenSource, ExtensionContext, extensions, QuickPickItem, Uri, window } from 'vscode'; import * as fs from 'fs-extra'; import * as os from 'os'; import * as yaml from 'js-yaml'; @@ -21,6 +21,9 @@ import { import { Repository } from '../../../remote-queries/gh-api/repository'; import { VariantAnalysisStatus } from '../../../remote-queries/shared/variant-analysis'; import { createMockApiResponse } from '../../factories/remote-queries/gh-api/variant-analysis-api-response'; +import { createMockExtensionContext } from '../../no-workspace'; +import { VariantAnalysisManager } from '../../../remote-queries/variant-analysis-manager'; +import { OutputChannelLogger } from '../../../logging'; describe('Remote queries', function() { const baseDir = path.join(__dirname, '../../../../src/vscode-tests/cli-integration'); @@ -37,6 +40,9 @@ describe('Remote queries', function() { let showQuickPickSpy: sinon.SinonStub; let getRepositoryFromNwoStub: sinon.SinonStub; let liveResultsStub: sinon.SinonStub; + let ctx: ExtensionContext; + let logger: any; + let variantAnalysisManager: VariantAnalysisManager; // use `function` so we have access to `this` beforeEach(async function() { @@ -49,6 +55,10 @@ describe('Remote queries', function() { throw new Error('Extension not initialized. Make sure cli is downloaded and installed properly.'); } + ctx = createMockExtensionContext(); + logger = new OutputChannelLogger('test-logger'); + variantAnalysisManager = new VariantAnalysisManager(ctx, cli, 'fake-storage-dir', logger); + if (!(await cli.cliConstraints.supportsRemoteQueries())) { console.log(`Remote queries are not supported on CodeQL CLI v${CliVersionConstraint.CLI_VERSION_REMOTE_QUERIES }. Skipping this test.`); @@ -94,7 +104,7 @@ describe('Remote queries', function() { it('should run a remote query that is part of a qlpack', async () => { const fileUri = getFile('data-remote-qlpack/in-pack.ql'); - const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token); + const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token, variantAnalysisManager); expect(querySubmissionResult).to.be.ok; const queryPackRootDir = querySubmissionResult!.queryDirPath!; printDirectoryContents(queryPackRootDir); @@ -155,7 +165,7 @@ describe('Remote queries', function() { it('should run a remote query that is not part of a qlpack', async () => { const fileUri = getFile('data-remote-no-qlpack/in-pack.ql'); - const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token); + const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token, variantAnalysisManager); expect(querySubmissionResult).to.be.ok; const queryPackRootDir = querySubmissionResult!.queryDirPath!; @@ -218,7 +228,7 @@ describe('Remote queries', function() { it('should run a remote query that is nested inside a qlpack', async () => { const fileUri = getFile('data-remote-qlpack-nested/subfolder/in-pack.ql'); - const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token); + const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token, variantAnalysisManager); expect(querySubmissionResult).to.be.ok; const queryPackRootDir = querySubmissionResult!.queryDirPath!; @@ -280,7 +290,7 @@ describe('Remote queries', function() { it('should cancel a run before uploading', async () => { const fileUri = getFile('data-remote-no-qlpack/in-pack.ql'); - const promise = runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token); + const promise = runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token, variantAnalysisManager); cancellationTokenSource.token.isCancellationRequested = true; @@ -306,7 +316,7 @@ describe('Remote queries', function() { it('should run a variant analysis that is part of a qlpack', async () => { const fileUri = getFile('data-remote-qlpack/in-pack.ql'); - const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token); + const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token, variantAnalysisManager); expect(querySubmissionResult).to.be.ok; const variantAnalysis = querySubmissionResult!.variantAnalysis!; expect(variantAnalysis.id).to.be.equal(mockApiResponse.id); @@ -319,7 +329,7 @@ describe('Remote queries', function() { it('should run a remote query that is not part of a qlpack', async () => { const fileUri = getFile('data-remote-no-qlpack/in-pack.ql'); - const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token); + const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token, variantAnalysisManager); expect(querySubmissionResult).to.be.ok; const variantAnalysis = querySubmissionResult!.variantAnalysis!; expect(variantAnalysis.id).to.be.equal(mockApiResponse.id); @@ -332,7 +342,7 @@ describe('Remote queries', function() { it('should run a remote query that is nested inside a qlpack', async () => { const fileUri = getFile('data-remote-qlpack-nested/subfolder/in-pack.ql'); - const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token); + const querySubmissionResult = await runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token, variantAnalysisManager); expect(querySubmissionResult).to.be.ok; const variantAnalysis = querySubmissionResult!.variantAnalysis!; expect(variantAnalysis.id).to.be.equal(mockApiResponse.id); @@ -345,7 +355,7 @@ describe('Remote queries', function() { it('should cancel a run before uploading', async () => { const fileUri = getFile('data-remote-no-qlpack/in-pack.ql'); - const promise = runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token); + const promise = runRemoteQuery(cli, credentials, fileUri, true, progress, cancellationTokenSource.token, variantAnalysisManager); cancellationTokenSource.token.isCancellationRequested = true; From 69b06ae95cda0e7745ecd051fc1e58ce0ea97a58 Mon Sep 17 00:00:00 2001 From: Elena Tanasoiu Date: Mon, 10 Oct 2022 18:30:27 +0100 Subject: [PATCH 3/8] Make getVariantAnalysisRepoResult return the correct type We expect this method to return a zip file which can be typed to an `ArrayBuffer`. In the following commits we'll read this buffer and save it as a zip file. --- .../ql-vscode/src/remote-queries/gh-api/gh-api-client.ts | 7 ++----- .../remote-queries/variant-analysis-manager.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/extensions/ql-vscode/src/remote-queries/gh-api/gh-api-client.ts b/extensions/ql-vscode/src/remote-queries/gh-api/gh-api-client.ts index c9bcbd7fa..1e9d6c290 100644 --- a/extensions/ql-vscode/src/remote-queries/gh-api/gh-api-client.ts +++ b/extensions/ql-vscode/src/remote-queries/gh-api/gh-api-client.ts @@ -77,12 +77,9 @@ export async function getVariantAnalysisRepo( export async function getVariantAnalysisRepoResult( credentials: Credentials, downloadUrl: string, -): Promise { +): Promise { const octokit = await credentials.getOctokit(); - - const response: OctokitResponse = await octokit.request( - `GET ${downloadUrl}` - ); + const response = await octokit.request(`GET ${downloadUrl}`); return response.data; } diff --git a/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-manager.test.ts b/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-manager.test.ts index 6be98575a..678c4d300 100644 --- a/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-manager.test.ts +++ b/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-manager.test.ts @@ -88,7 +88,7 @@ describe('Variant Analysis Manager', async function() { delete dummyRepoTask.artifact_url; getVariantAnalysisRepoStub = sandbox.stub(ghApiClient, 'getVariantAnalysisRepo').resolves(dummyRepoTask); - const dummyResult = 'this-is-a-repo-result'; + const dummyResult = new ArrayBuffer(24); getVariantAnalysisRepoResultStub = sandbox.stub(ghApiClient, 'getVariantAnalysisRepoResult').resolves(dummyResult); }); @@ -108,7 +108,7 @@ describe('Variant Analysis Manager', async function() { const dummyRepoTask = createMockVariantAnalysisRepoTask(); getVariantAnalysisRepoStub = sandbox.stub(ghApiClient, 'getVariantAnalysisRepo').resolves(dummyRepoTask); - const dummyResult = 'this-is-a-repo-result'; + const dummyResult = new ArrayBuffer(24); getVariantAnalysisRepoResultStub = sandbox.stub(ghApiClient, 'getVariantAnalysisRepoResult').resolves(dummyResult); }); From 7cef45c434325204007fd38bbf3df46ee4d697bd Mon Sep 17 00:00:00 2001 From: Elena Tanasoiu Date: Mon, 10 Oct 2022 18:34:23 +0100 Subject: [PATCH 4/8] Use real zip file in our download tests This matches what type of file we'd expect in real life: a zip file containing a sarif file. We've copied an example `results.sarif` file from other tests in the `no-workspace` folder. --- .../data/variant-analysis-results.zip | Bin 0 -> 44140 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 extensions/ql-vscode/src/vscode-tests/cli-integration/data/variant-analysis-results.zip diff --git a/extensions/ql-vscode/src/vscode-tests/cli-integration/data/variant-analysis-results.zip b/extensions/ql-vscode/src/vscode-tests/cli-integration/data/variant-analysis-results.zip new file mode 100644 index 0000000000000000000000000000000000000000..ea610970b22625c2c05b92937a9a80716a9ce04a GIT binary patch literal 44140 zcmV)0K+eBVO9KQH00;mG0ECH3RR91000000050kR01W^j0CHt>b!>EVE^}dWX=YVa z4FCsOL_}j`~gux*ey-vre40o$0$b$rIBOZF3@t3Q5KBr2XG-y#XkIpd>r#**VYIoU_vw2?B*e zp-@$*m(Tx{BoDr=$5;6*dytHh2UktAs7Kq|Rd$_T7R}Z2d|Bt!xSTinyh+F9Y`e_r zq8>Kmdb`f5;$m1Y^6~a8tDC&qKB119%)FwX!#}3qrw`M=*5&-c0LS~7S9JkDa6Ed) zkE`Xp#%HgS&+ro-G-Wx(2mBKrOsWFD)sNueJe%dz)m}Nte?FwXPCI0Ve4)($uF|y zwAm}?7schWvasrbpjkf8&Zjwz*Hp`V;J=;bA9F(0gX^rC7xT*p>iw-Z-l8fOdDRp= zpkKW9^=Gf?+ zG}0fMXW<8nththQ_-FWbzh|05HJD`4VA$S(P6Pz{ntZ1|+869OpIc@&AOUs<(-1 z;Psi+z;c@xu&1jK;H!KJyBBB-et6&fjC=KY-oTnR*&sQAr%#J2A7kfXCk4=W1-meh zO#86Byu=y6TrMjh%Htegcz{fy39P@5t#Y;~c*UN>SJ`DA4m#t<{%AzEFSDxl2H?YE zL65!WlXE&?^3(D#n_n*dQ3N$i3)|m)_ht_CczTs5a@-{4Mbf~}lilNkXURob!9fKN z;D4_3B&+gdS;MSS{A~Np9G1Fnxb3@VN5{{1Pm>+lXCLXzYLCO)0=mg3NmC|xE+U!l zo%}^c^ZSP9s^@Y2(eLruG`p;^p9PQu{x_RW6Cl+D&R{sQCbi$&;n7b&9lZGI-Lu2p zpH85|*S+D#-XQ4>hp=43rX1D{P-fM8+X6fP1b{XGbGd95%LdVOhP&p&GywuAryuiV zTAWu|bu&aZbY{kd?{HNtF)<0AZF`cZxY((**wW-i{>UN&;JVG zBNPCY8E6E|dR$hMA9&h>@TUy?_%WetUIN>j0!2?czc(LH`x&sNVlmCbuj*Bw-3RZ; zn$F>D?nd&{bHp&?wJw*{I6oA@;ANFx6rTdJm@a2?WUAid<-EXgU*y-*VvdtI2Qf0w z=8s&byA>R~AM^jOSa@H0FOSQz@!lfzb>2Lq74_c04y$929Uc|+-v@Ea#buRE^12&w zucU=S-?%CkO&#pKi(;NlpT@Qy>;rgMpJvr%-aOG*yVn+*F0%3a7QlGkBtP>z0(A$t z`fHSu4J>aRz{%~sc$L9=bZkr8cNjgvw#X;&5XsJAXxy&P50e>;JRRU}m%%5nXIlWx zrbB870JXq*K7Ed_3@}91S2-YOIrA20p?D~+hy`s!q-+3#!w9z=Y2fa=7$2svZ99kn z-q;}x{`6U4M%=c&qO`^|zo_!&48HoX1R19WS_Mg&kM}yqv+_I#ITQY#15_3DK~1uA ze3VA^&k1e)kogEJh%kvxwsfD((&Tj$C)Z=a%PF=1OsT!>Mmbs_BsB^dwrlJ%0foAo zV_=uZ%encOX)hGKfe*BVal`UJOoZ1gFNPD}|A?r+fW`5)#AZ;TEIG{)Wdsy{6l{h% z;Oa)U97W_%VSGx4cn%OTR_}i<|oD95H^Jg!)YE4@%Wx3q(B>a8de!smAVUh zD=#i;I!q#c!L$mHcaZxM5U`^`D1+TV@;r7-+x?=L<|*iLWF$eEX;}fw6%N6pP){Jw zJy~_m!j~Z3fFuo*M^XU+4TWjYFi`xeo2+VlTFpP@<7Ja$t0A{K12CyKpkOP7C)Y)D zg+~HHTtcfWP^%F&)FlF$CG-3``D^wuqmMrpRkMV(oq;3^WRv9Z-=ryKFiQnxHm%D9 zjS*Ejgx1@16Kpl^o=s(-;-2vQ3*2!`X-?Bd zqQU>?*323&gc=LxdNW2lV#fd%snAn6|dddWY2 zVZ6RrPNyein5ZrJ{ESmZ9A7LO*i3fT+`0UsMx&(P-%55KCm+jVvL&nlyZGfQhY3_j z1>77-3)Xe+$Vy%u%mpBym3L&! zKt7w2k?}urNLisNsh0~<`0aQGra+6{J3JV8upwM0=3^^n0f;G&;{#Z?T6Qv3gC~>? zkfbe=h=ygKvSlcw!Fk!(MZIxsiN2hAvPOHzG1-6Mbym#j?8RLSJ4SHt-t`;Uwp%T< z0)@?CbI>j%i)~U|&P$-yx}4=GV*w9Xl#As8wl|O;>`GwuXgsKrE3_6)i}8Dy#hV8= zdHv>rhs5PMyo8NkK<_(nq)hAF08LOW$4y!F=?jbTNyJVyid(Pz568w1HgYDhpa}~D zMi#(#TYTWa;SIP+^^Qc-1aCwCrQK#~Mz)RHKk%0SNVDm*9QVK9l6J7#v=C`*qG>sQ zT1@t?fYx$7WecW^bC++0vDglz3pfXn@mto~HvrleW^=++eAz*_Y449{^IOyVZGZ*4 zY8rg+jv<`;`Y-+gXMD?KKcO=#?{AHW(R;*)252FzgNtKk1_Gjd9dF@xWf3b!mBJn< z7pEW~6Ic8*obt#Tja*?4Of&~xfsimMuji@xkou=ic?02p`n{8UK0%Iz?Xfv&Ie_Ax zLu*srv{u0!p>+dar`J`{YPny2BF9zGf*M337nAA5`|I* zcnxG{Cjn3-k)w=ndZunbI=g`}KpOQ2Nm+EQDmZDb0fcWq=eK7Ypra`l3nQbMfj|hq z&TdkT6-82eI3A081Q4DP@E1y&d{0xSngCI9#Q#s0@(k79dGZU0>E$&@sE9s3%Td0v z0N@<)gq#$}VY6Xwhi0|1{1?!8XF? zMF|8N6L_=wWg)75v(>#urZ^8%uiKz?!i%)eCtlW2ed>9-YW9Lf^2-* z6eOcMk)u9!REw+RAklz5QbVGQmQ@vqH)tcOvP-sXkx>qX5x+lv*176hCS!94pMq%P zA%;LB`CSU5kQIZKn}fL9_pa!-%u_|a4uP&4_P{~3BOm!|Sy(j|bowykG=#*noHxa^ zHAW~bu^qYg+u?qdxuw&5ehG)zlGTihav#{&n2ptV!)tN zx+yQoI_{b=Ke~vR;e?BMn=csPdG;Rk5H!?Zqdqk&KQf*iG9HB^v9O>9Oq-IKRi801 zNFEOKYen5_wQ-bi?Cx5Q$9X=1L&ig@2v7({#AZ=dq1EC)3&Ed%*cNou8WX|<{#Pyg zCQ2Z6&MtV=JzF071#z#6e26mH8_c+tWHf+-)TwTNUDEYj&<7B4LOw~8{I4}X+X`0! zb{I)8$&MyBe=7>&k)ilI_KA{!!t==3T)zDbJ>8yd`CpDU`m#lT4ydh=TDE*6jhPqh z7}VMSutRGqzkdBS=>=9X5$EWe)%tmq;O+!zYwhiJnF?njRU0oBmJ3Bq!6C}qYeR?2_j!Z&x zaz&hrtnr=m-q2@S)*ki}SKl3J2E_kZWK}k^AEHbySL2JwIec?$Iv-pT^g2J)uc zPS#V5Yv=zB=QR8EtU0ECAm7^4n??Tk3r^~Yb^NzF!!5jB$M`y?WhDC5Qj0m9^|bA2 z+?tf-qFbGpw9N0w3`3!4eFAF>cbA>gdgDNN8E}TtZbeAf0bd%*s4%TINblRqLr*@5k^YBk^n%#rg?9_A0&BtnObk&CTrjbIM1pm zLoor#24tL@|ITngz+C@zf-psYwj!Pc1L_CJ#>;W4G3jP!pnptHh6tqR_`ry*~kT?#b2DM zqVt;5XJ&fNTPd3RT65AOYZpgKj2#S>(RFMTh~OD3^2Rrx%+ush zgajg>VN9n#5ff)kx3Xdih+_T!1V(LLAO2jIEtdVm5dq4w-xsoPhwdG5Prn;OSD=_3 zg@%F58$0xzu!C;>R<@hO*lp;#Tukn-TZS6YZ3V1I^T>@nvv`Sd+6m;+u9KC`EFS*b zST-Yghl3nnO7`TTN%(Y_w|;L?PX8N8o}K=d9LjiQvjwm_9a6dp_h(6;!tZZ_y@}^( zy~u#M^JWqk&2j@;xRqc`nc#V{Vav;=+EwcaqcRH&`BM7Fl&DT z(16zzDMqho-Rn9Uxoeh>OhD0!C8uYY%LP~O)$xIHDeYc7nvKiAuxbIJ>k;OP2^Y`L z;c%B#bUef*jBplTGiZ8If5jD6JYWsi@fKH7T&!7F0vRn5b~albi;D?uYVs1uJ_45@ zD<%j)6uFGtIun_1LOEy3)?#Br+NzjV0aw~KOfXFlo#S~{0T-j#LhLqr%Hky`5ef|~ z*;}eS;&`i!DTa6`in?z=iEM5!+FY`0`>b8$RA^eCX9b$Y*A15W@dJ{IGU*QJK=)Auy5!B zI)WLu6AtQnnUgqPSXa&|knV7dscEzn?|V0Mc&~J#f=*4kAnJq}AmztMxW7{CU%q*m z!-c6;VQk2#q*6~gnd)JCeP2spu|$f>kkSYGc9+#SCvWzwGGCLJAmSCYG*&XY*`Q_k1sNH#nylz=2P>rE{&>^3Gy| zN#Q{za`y}E&^{(n)WU&~C&3rp90^#_a&F8Cr^4h7aYU@v=2hEz-M*fSOX@%64U1?q zNfx(GMC+EYBZ@1tVv~oizgChqEiJdWqhM>l_r)JGH{vH2h_?~p$F>Z4i!aZbY(FA% zaMTAHIh;5|@nt5N2pPv1Nl^qz&Ygbd)4$ut3Q;;i&pq(ueI7qHZF(R@s4I480Q6Su zT(tWfn|uc3G;0gLEyI)K z(qL;-WuB4@JfFBXg~~hClr4E!LIadB3`rB12Bkm3B$8GPk=bYz4x~E}?29o|>=i~~ zl z`J~e2W}t(%hOmT2bvGZXvn3t?x52)&;QOo#M|*2bM`}Zg5&Qo^oNQ!Ib9;6K4snnOWRKCH@XM8A2h z2;nVeG=3}m&B#2aI|Hp|e|I4k3qKGG_Jx#!Yt^CPXOu#K5-9Pa`^Ouj(xhX2H9vbFPi3YpH==ARTqR6|}PZj$ii-2-*jk+Je`+ecAc5t;m@7NvE$#$o1o`$ePtw>~TSw!onsQd9 zA27`=?+?ngMS{F!Cr$o@? z2ezmVvH$J=XFQuQHyg?akyc{7yA3omGKD|2%W&#$?%diK0mMP?u%S4Uc}+`!E74bY zb^9k6@(Z?-wMg#Fvg$qO^-y~x7^ri*aJya$vQ>LW&Ft=b#~{ksDS3gSyGImN64!~A zC^ZL9*4`a8uzLd7RdK@OF~Vw_;2% z1RJJVUvD|Ewt%zY(15V>?Pv3+1iB6|az`5hcxJuAYp<9OOeR&cHF-lhR%LYKQJ7s`c8@P8 z*u<>)N=|?bcdmDn(Qp?4RV^M zN1TNl$PsT*`liWNI?ERQ{_FSo&49Cbza=-Kvta|o3r}z1KlI!NwP}tl3=4D)omW0c zC3W=lXv7BFi)nTV&}DOb(IMf-msPo3Xl$4TQT?3XVBE(Hg=te;BgFt_O!{N(94@YN@`zEr*fCg`;53KIDY`LmQeKovNy+Wh_XH`@A{^m>%wb(ja_^?z32o(& z8p{^1!xRXVYr`e!U9Y&ACqx(g(*ynyeVADyTYj zLB_xlx`N7MQjK0_t$bI0_1xA23H@~W zKAKi#*LunrJ4PX~hR(}IdNGwkLVsLLUh)+5Y$+^q@v^Mz06?{M zOy43?U#@pz#byT|1XGI{cyzUv$n6E{KdbM^9dKLZ6J$^aL~dr%HnbXuy6!o+mF_F} z&T?$>XhBC`cKX`x(&(C%;fs5Rmil;I<1i(mT$ zlA1)__wkw`Jc(Y@tFlJ%J$;88b+vcZu##-2YyWaf_U9cC{#b?pR-*VtSFF?;zdJX# zF3&q^vB8#+Wm43Czs=&`zuO81o6(B(Pre(q_>SbR$bq8*ufWtjyfysq1CMR<&brAd z7j64)oo|KN%`x18M2Ndn+sLV4QG3Q`12i$^pYXtk@nw)4Z@X}UsL)J{_j&T>!Kc~u z%>xn--l*cUwxmI8xm28!D@(y)c*wvutxsViv^sqkI|^2;a4>LrG9&5geLJ4XDNLGq zEjk>)7GjcZYICbj22-dfHu^`r$N(i<5# zoyg<>athqar2Z{H(~DvXSnA_^QO|7;o{D5yQ=1N-i>@K>#8pgfNV+!_f0<>`gpxZw z#?Mt#(-3|D4%@z|LsJOVqmUxZeu*D3BMPAeQ@tYcGcFzK8V+-=VK?Qg7y0 zbm#Nhir#Cb>s^$aZJ-mK+{Ye9J)s+8@%UDxT!LCi)qA_krk;VGn@@W2xjKm(=rD(&kbFv;GMm(%YXwfKuk!!_o6tElV z-Y?3R_KVJ3^>`!aI_}Idzv0q44=K6!p+z#w+O?911w1tTwG>%EIGHuPY|P#L@9|nmq9q@q%1JO&mYtOm446Lh0Zl|%w=<_;cSY+fz(Uk1`iUEes5Ts zrQEINNYtzohFIX$EM|rh4pCbTBKn06PX>g1=8zO!%QpE{d&I8SL)6ll8LGuoZaIc& z5nMed0m3AX5KY%PX2hPBbIhxR2M0+ZOo)WThj??0p+^PgqhVX2%Mx68)zR?ozJ-yw zu9?j{qQ@7WDAEX06p_n!@*<_CHj>)U5aBnCgjM*LYEO%4|HHI=R!nox-%CGO%|}Ro z5*L{bY-nb8;=neEE<38cFXj_s?FG5?AU`so3=#qIt@(sVKiXom8(w$nnm}i|nPJZ1 zPF*`Ycl0VsdU|jeJ$FN$%@#k5Xk|hiVfh$UfXvf^E?{M^Y=0}13i;jx%P&?D^yM$B zE}61fEf=rSob|-B*M6!33of*mKo$rXqjhx;d~t=9(o+mk0(_rOy<&Fu)(*C9UISmI z&{8xG3^6(Gkg{$7YrKFRh>ihaEB$72P@ zmvsXQiXZ#|3*S=Q+!wB>p_m4J9^iBpk+=uanA8kXI3_tocI4HTW(IE!p?gJiDPt)W z&ZS(TL5VxAG2xqD=riKqO@jfPwMt^`iqldno3aL7QcOY>S9{Q8|8Pf+x_V?AkL`m@ zdM~nB5IidhYNXWf9g@`iUDEb^ouobM@)^?Pmy7uHIN^uM=bhL;&{%-jF(0{1h*Acq zHYw=2GF{jy94fXi>-IX;y|3=s)x7h1{;KJG!ZeOj585l3N9|lCWmA2-Rba^_+V%Wz zajCrr@w|=QSsVQ_tMtfUvQg4!N1kkcW2+vUc4`%#bT`}}0_zP*+IOkECB?_Lll?iF zn*mSf7*U7wF)0RD)y>iQc;dBm5RPIhos!mK>$}bGo2t|YcgHG_@F6rJnA;HW{ zpZV_WM17mrV|FFB{6qAg4 za;hq-`?8#RU!kJiW@oF)fOiM(_eIHK@jnsnx3+ekqm1wjQXiSEeC^lRK8fs;kkuT34QdOO7Z$7-BgETc`cq^9P1HrIZuHyQFeT#qNLs zQA#9Ar$MO*B%221VrM;o8y zeO+9w_2Df0WD6!7lCo{w!M*n9raoG)=RM~ey#t`%Pw?`gNUUR%a~QDM8nL$Z8Aj@1 zc+*2>q&9HqPHw*^SMnNVBV#Dof?b07===R_kerj)^c?L0&=EfwBo8}ozjhaR1w-hg zC6SVG9307|uB4?o4>5L)IXL|#E}L|iP+u_*jmd;U-um>3tqkZFU81QA!v7$*wz9rb z29Lh(n8!(RiR_MRkpgfz<+Bi{jfK@Ysnq;AX1#!466YAdMEX7zA!1M-~l@RhDl*+rn37h=Eh-KlKo_XNcIDc*~uv+{4nbeg4Qb-A6- zhp$dJ2>h4)eEZMY$Lxd{&GvB)sKOEp+doaq^KAN#y;ACJ-pku=7BUdWqysAU_8xA_ zf=$X99D5$TCwXpRQgP_~=k~`Mmrml#w+qSvxBc%A|H$SG^wJ>dO8-(HgVfGCm*tWm{WB1>!KVY(-|tz>ewhS`?;$mx|S z$8q8MbUtjZ_r42G%-wd`q6kHu<%ev;GK$$P;qZEsU*-)~emjJweQS;NiX6!za7pLB zY-Pvxf7p?S=RwWN4i>I&W|7P!E1BZ}gb9ydc%IZKMX3WZRiviC7`P4xNeJ^+kU5?q zAZ+bu)+N-_e;DcFk?ugVyJ!zBto^~kXDG1UWBYV<4TQoGX!YD>yElJ?9sS`C9ar9d zC9AfT957yY$}28^l)K9Pm`zb^L#(PZ+qa@pH+B!mZ4x#Ls@4*UW{MslGmC@eoVs+y8=hewuVVNToeb`dI*qXAD`4MsmZf6TB|aV{$5K3N z;K@Zs93r)7iFR}46mBmg(R?0GS%P!DW>iEltvYOcN8d^1Jd zeWA5E2+eD&&hcOajaYZ!pwKJ#B1bKE;TxaSO`U#^^A^ozUBjx8WCA8|r) z9z*8HbHX{Ag^UV!P1}h*hIXDF946NpCFGNWb!|adG#3OKAU-)bdhza;gVR3&)O$zI zUmhIpzx&UF7f;_|S8}fAv%jEu<&Wt(M z%_dME?!Nfx)$ULGEm;30{eB}@Ltsqw;F)oAGf{kG@^8m9o>2moJD~aKmlr=B?>^l> ziGchs8-c7YADnkLCPH_YA#0SOF5r&$PmT`%vj6Vq!{hyDE%^R*LzLR6%1YM+ZPVpu z`1%o``sv{0^mz9a9^QMMz2);f+0P`a7am2OPV)f0bnWJF2gL3k9v=Pj?(pEp<6U5d zFL(F;^W@#Z3x{vukOvWp__w=E&IPkdM#D3eTq;2S9YBA&d%FAM?#ceUS1*2i_2TJa zd#nB1hHOEW!1GMD$Tu-9$rz-H2*KO|;ulBnULG56)`IqbZHQBJVr?u1_|a>d0du_n z{OEMm;{NmIXfp(9b3K2Z#We)@{P5lLqo@0aZ7BcOU7#Ee%jsl@S+p_LTH|f4FbVYi zfj&BX`fmR(yNB;iPIpgZ5O2mtIiv%Rq0_pZ7Ho%xLL39}Ws=ye*Vm$~t7sA)Xh7}angJ7b=sMyQb1i}*t*2A$u;M!LN zZnHx*mR-uMoSc}t5c$_#Ltl8=02G_e`Ky8v9^oqh@uQ2rA`srGyaIq@I(NS^z4%A* z%F+D_55L7`vWM*oIF6>1eK@D%;3K=_B}3J6}BDV%DVp-VTz_ z^5B8V-S{9G*@y5LG)a8&Apg|lbBZV0{xBW#E#G!sjkoQKw5cBqybphbU3M^`&wqDv zymx$bbox8pq`=z)eEq$F%DjAFT$loA&_GE26*E%h30v`^>#e7pa!>mpad^$sq6V#* zY~9Z8LgXs8P6k=+R}R{$$;tEtE!;?UcyF@FCN{_mj4XY}M424*mAxT6`3B4Rk42_wGh0sYp zRCGMz`XvV8?o)0tys%A%;v1s?w(wh+dhAJfs ziESM`_fTYxdD=Vn*K+{>(pz(*%h%DbUme7n4wo%-p60W~)FickYTY@^>J3_Ltb0bZ z$;%2B5J=R#3~+>u3A6k+wt1UKXgT+v?Y=rZ1pyxfqk|t`p=jhzYLL74TdR(-Xsw5W zw$}4k3&2Ufca@LdyGS-_6%TY<@ydSr6FHLXE(-FUQ;&{es6z_En$Rna6CFtwA_3OI z<~X75fWtg3D&hN6j{CSOuYaVHDtlLDF$RSzo0gaM)h_fwvRLWjeZ1uyt?;iT}IRmq{TSaIO!voU>kQ#_q6n$bQC%NZ8OfbWU)ol3V|1 zkP6BA8&M&E){d8R`e^qOeU%%-#0G5X%MXwz#V1iM=QfPrL*pS;DCAT0S;k4f+-LfX z^}fTeULD|i!Db3S@VO_UJgf2*^d)u^^WR~d-YzrDw)yjvPU&zP(7EiB8e^mQTYj2P z^I6^mGm~c_HxbzpP;Cx2?+I>SBl~%R;9fSJo`aw-LS9EJW~e^5SG~7|0Mc8Q^^aNg zC(1FS(Z)dn!}F{PWS_k%pI`-vY^w2;EiuHmDOig2hVnGU_WG~^*AP(fb2#Jn9Ua~< z``@THv(3D&UL7BvNEV<9gXm00SfitJ4l9Ul^<-kCM?1aFka0YvI+5|7xW$lSnlxaX z9LS`RdC6&I_@QKV!DmlZLKjWkMtAhO+?{7r;4-x&VqQDROxhbZwrv^spT7jeoL-p^C347j6Sgd!=b@f5Ec8iBB26Gty4L8i6%ITzLXZhznEkzb{8@#Hh)D^m1}@J z{c8;n^{-I)rG6;Uz!dHD%QZ@otD6xyQGV-~x*_aR) zC?6M6p>s(b-iEft#>dqxYM0UJsG?&512Fvj7;a5%RurQaV_*e{&7c>jo=Z0Fy`V8P zb@mOsv9JWxR~>G7Q$Aoq1XNc^V{+zUYz`JHl$~lxAiKb@r1(y%B^YKvH8hzVu(~H_ zNcLj#S~(Cnsb)UeGXot8J=>8H;X*22{tH9tG60gV4Ulo+ZqjJq5<)*5(aWR~(pQTdbsB6%DuI3v+XPi6wP4?VR)i3g3=>~#fZ#la7am4YSSnoL&nHJOFc6OE zImhDH{4JLb!iqvSBbIJ88k1W>A-y)Am`i4hW9lO{TsA=fFe=a^*s zVUb4-M?wzTpn%+pS(`}%`4DNl!Nh*$8X6z@F5LM99Ju5PUh;mFPL&&OC%OL$3pg&T ziS>i0JVuYL1N^G=5}Gf%DW18+2Y)XHJn1ytio-;=Wmg=`*>k~Z#fNNFLA=HrShd+r za*){8fo(PwQtErI8WcJ{)pl?Ow$o1D_dCbjWPC`s8h%b{DOAxz-M=@mAB`{xdk-(a z!qF{k%_j=8F%i4M?z?g9Q02Eh9G6uR!2sGg^ZI+!9qN z2fKJnvkA|bbAj2uC}sZP2jn8TyKG7$-}?=`%VNv$WFv`E;H8Sw#;4PwndUY*0R2+M z$!J=m&IzwhzBnu9C%`d5W;($PpNF74tBU~2en9o|0(2!$D4_D+|LtEMn!q#eZ*K4- zEaw?a)%CI#tH~21pRF>v!^lEtHrs&`%tIb8ZC0izh-}j|Ao$bLo;TdWyZGp@Mx>V` z^cO4^WD8PeORD*UO~dovGcn-dZ0b}>a*2@S9HQH=FqcvHT(7dam3>h*g5+P6bP2J{ zi)(L@(p|O)&|@qqHU<%KD%L`k+pv|%;b}3Mzj*^3Vgb7vh(PDn5tPv?NqX8$exYZg zF+#|fk^xBqV{EF~Uh)Swe3G1f`+4;`-hw_$FI~zD{C5sxy?=B+q`X?3mLRa^3J<(a zv8~kaDGZn?qE)Fx0XR>4Ukug3oUB~N0pjjZp%P=|QeZC$b<~9;*?duao)# ztOJ)yYUi;JK#Zx&He7XV35E8fbnAxO+KR$@UveW`r@wT8l-MRXkQGDTPj=;xo9;fJ zW8Zf1oOZ@tUCEP2h$Xh&T(axmS7O6Gx!#uS<$(_J^vaPF*tC~MMsOHzRifU+gpFLp z6S7!kIk2a}DrxHy>*1O#P?As6xIrPmmoUIEa-0W3BZk2s_JM*>K%3D2I>PiNL=hWL zF^+ksHG)C$lce`5f0@fVM$K!aSp;r+!xC`p@}bhsO+EN z2MMRpo=8FhcVqhP=U~FOY4Va7F;`4QcNQ^&2BOZ{4cfUY+qvvgRT(B$&C|+2 zdyJ1!aJC`CoSCPO!NtZtv~{c5z6b>!crb2iYv;c|2SIjDm#Yci%O$%n59I!76MVqL z6LtRCvnV#$tGU@0YkRsyO9{!^b^>?0dR%j)1sVE9ix0Y!J>r#=i#rJhhKLOm@^vKw zR;Z}=FAfl~89$@aVJ^eJP7J1ml)iav1rvDyG<Dn4r|=!hQ3A-;I(z%m1*M zXriE5nGkqiDe3}pZ&^3x3w(J7e+Grv;=KlO+QI&IasQ61(6@do4Za!)*01hh&iB1D>U5Lp zn@K;uO|5YxNS%zg;7wDUYc0W*40edrDkHg`ww+W`!A?QW9 z89Kb%joztG72XuN2ILCk*PRv7AhYe}*A4*Y{It5+HMg(!SYD~6VymxlCA)#D(s#;; zHKL37L5qDYLI-8dH|mDFalKOJU~|Q6%GtDU>5hFtxa)WuMi;q# zY(!ow(Ra;!=bkga?;U953e*vr5II{;sa(&FRqF$-&Hr$GRo3}@*C?~R0{6UHh5Z`j z_fF3SzIcbOz4a)0-GcY6Yjvx67p{O0y?7CW@-Rj-Aor*~h~V~qNZ{2l-g_N>WUEi6 zsFN92uh`IEvDMh$u%i`@T3$(uE~!8ckTuZ&*6i*)j?r~qxkKCI$-4DV^9#;$&H*1( zEdz^SG|~^MTy_i+l_D<7ih?X=vw&Ob2zW?UCZm%1~Rj?&b~#C-03n0l;EIl}SI z>x@KBIZm$gy$C0fr6$3474zzVX|rj|p^L>9Idm|bv30-|Ke50Iw;OKZ3-nDG5Z*Uj zgW*~`MGHjWUSYq;T4Y!T%7@7%8t_4TbzpYw`AqQo!PEm@O9BY5l_bedW>>%s zn4&9hfAs`y&w!iQFth&G#M7vQdJ601R8+rI_|^&p9n#M6PbFw|p4U4h_k zKdEm)47JUvCG|1zBj8hLH8$zO6)t26pB6ZCsx?dHp0t2N4PI46dQo$|qn?S?*4yR+ zbL3*^A#~Lb098P$zq-E1@&(IwHH$2a**^h&Dw6fVaOA1quUai5U-QzS;L*7>SlMlZ zb&IWBRuwf!c-TGm!rRE;45psq)i=oMi<|@OG4NIVmT{$n$Rd1{Hl)q=kZVqRXl0?N zCT>GtjX0O7mE?p$#+@_orZgRIv})U2ZN$nXKA1U~%F6E13OqK%62@6Gr$zeEthRLk zuH50+{ifm~ z;y!!jE>$G34j&h|LwUE3{LQ-x9ma6b_!yuO2f9zsT`oS8cGeIJzr^0yU<5)s5qCmGJbV?%cAtTJmjUGkoGh)nLt%15uULBYryqC%oK|}Q>ZA$Ue*|MK%Pgqz#wo<^>cVy+tXBmMMcUj=^B`j3< ze>`WPDk4M32WddJJT{gtv8i&GDTC7Y++fZ5NIYE7C2TF4p{MWdxq*l2f!nDA-U$v& z8HZ^NXXZ1ZzJg$|fea>7a{Z%7lueXyqA;XcwsY##jMAU@y6i>X{cw2R3OKd*QD3i+~9xKr1@Vvzn7#n5K_H)~L!pwcX;ZbpmL= z$}a%)tJ5maF=1q1BUfm{S~f$jsig~}TjkTmDJ6PC)9h>kA6F-ETK7MrMg5}qgdwZg z=v{A+ywCH6G1CL1ud|DM4^Qdkg6|y6%~FSS`PoIZt8vqcjKJoscIrYPHIRWnzh)f} zzV3I42CGhEAQkTeJZYEnMY&i`v&ICd^Ccv^W>n?!()fX}QF@U9?VAe%iO~Sf__X{P z=R`GK^#D)&-=aq+BXJjHA()()_a_BV{*!c4j;Shiy0mJyx>8qT`C`N#v2m3d2Uaqt ztZ6=Lu5vs(K9b?)w0u4>sz0(VJR$W6pzf!%RP3mm&L+Kp(P!s%IbAlb-pG)kSD6mq z*5Ki}0aJz^$liddsl(nodA%@n!=gckp&S^wqZV&g-Tkf&@R?=Rdn^n?V(?3*$n8_zjU4$aw|=Sd%A-@9wo{U3Lrt zY-<)tzWuE5a63wV%q9tkuFx3JJFhn>bgA~*0*Y1Lmz00O>`CKNW(J+m=jc`zBKmgA z^K)CN8|jpn$!98+_}s+Lo>Bd3`~LRLn>jtXJ=1sIj_q|TBRCVA5OsoD>U|jt%keaG zl;7p^f#(>Ae{&1TgF$;zq49mt^B6aa}ZcmNrmPRuC zVlmoSS%L_mOH~sWPvg3*^tS{;%abJF2&Oah>Evbbl?h8D%phqYUY-g*`p_HFlF+T3 zWgf}20`XSSsNQ_NKQBR?#ON5@Xc=rIoE5LOxQD_cS$)Kc&|EF4SCXFY4Y;i>MB{uB zwO1(yS)zqW^KF9OMWY1UW>!8!oT$O*U&cAK-?@SY~+e-FMp`rkMIFy_rpQM{n(4 z$Pz)+nHPV{L#KQH64~Y7%BqsgipL^V96sGJ=z1P8&r16Q_jG8$4Y?s|Onh(A^m)#GNCjPMySw}i~ z108eUXd}yA9UK_WvlH(!<+Mfhqwt#`sjghEMDp$SWdzS;Zm%A@_iw8sXNho7xFo#{ zfS0(}jJ&}}_8ZUU*jnFXagbzp{l##KDLX@{-pPHvm8y)HOGo-$doajWNh>ihxIH7+ zN^Y;MHD40xtTtidza^Tp0t=~mOYE!5_|Z3t9iEYa(QlyadUh;r-;?|MW33$b6ja7K zhT$Hbo8s-jGp6A^SG$UsRL{lCrR*j|`7@Bk^w5i*Gncv&WwdQ`PGU$O0J~W%@~pCq zq;*-sFO<80O3foS9j_(^@AXOTSIRIb={6-B68-9wbu@pzoHm6d0_L&;{{Y0MXat?i z;c%ir#HT;vtv7IzRHLd^avbtN)mrMxlZp9Z;Fr5HsNsb(t=AI8U;;d|31(xhmjws) zSo3;QvZ!PWouonH26m=Yk5se{D*_<6P#Vzz`!FJK3JfdHRb-;Fr)1xHC-N8QWo^IO zO4V+1clcIPG19fJIiOveJl#|j0o_4>)HN%W^85aTvVcy`Z3ru^kl(R~yF0Awnq*w< zm8D2K3&^*hJuq(XvF>L+?2(oCBemKqq?iNZQO|sqEq;jdmdAkA1G(GxREz*SK9}Kd z!bBh0ISNYrF1!GtK3sgRZ8p^vD%yd-g1;Kx>^|cGg|D7x^BZxmPKwJp$WzZiY_E@&jWKa5JhZlx zfwb$kj}W#Ey2=z0Li@XZU%=5cQKW2=UrV^q3`5DuRshfYcs-G46SO5P$_440=nOrd z6d#KToP%=SZRru^x`_vJ>m$je!*+6Dw%+32>Z{uwfWS_;wQ&RU9RItY;8ExsxdZRQ zM<`R-*@AF*{uj<%d^woZVSj`jiubUzqrby@dOLQ<7GZ*D4J^#=aTlx8VcW#)>XET* zAv|PM?V+N-b=zW1n@2k0KZ*>!DAB)N`w7;vVHt29yfq6A2AUn70cA|`CM%{uyytj_ zOXNy0gJPPqPzIB#wM7yC-6) zt|*+&mVGJrqzv$z}(mG9sZ^JTok)-X^HeF6#&ag7Q*E@fXVbAC20x_B7$|V9v zP5q@X%Nk8(hn=wx1`AL*=V>QHDy*=7d4*56wuD6ul0a~`25Cz^@#?w%0cTj^&o@PcRCq8b#^K-R!ruKZ}h}VR7HXl&`ZJZ|If}9mv0uLUNB0XkCgr zz3GBg3klm>D`55~4f*~7-MVlnU7QK64*&M|^b)9tUa|52QT!dowUPWNYVptyG zBiP?~D`o#+&Jo*VusyJ9n7F=$a~cj+V|Dg#Ec_Ll3VQ#b2F$M{Zdy;je*|k{rv}Li z``_Pk{g1ZnUvc5bQr6(-{{{k3kI21L`B{G@S}=aA|2JG_d;w8-TtIHH!sXv!@w=8@ z09B_%s|qQH?da=Ad&;)fcin38Wetas_5ShKY+XDQ);P2>O~+Mr^}+aI#|mk-ko)0k zW@4h8Px2=5s4$KJo3u;VnxNViQ*q&?%MKr43DM*P?53~_>(gl9YTJot!vHyU%759Y!e8z1YZP&{4%E(RawJ{~|z~QkFju231>^){m#e zndFCf|C4@&9q14CRd=Cn(0%^;W*yG#%=Opd@JH*eQB)8t!m5dD~ zQuBk?7YHIilc&d{IJ&9R+r0~X^FPej)S>X ziI6m-lMw=+gN}3S_MRt&rOjD6M-;B4&krVeb<|PKbuF3tH0YMRJ$l~`7HC2UwtT|2V6Y**OYXRL*@E5*e?8NM^|V$2$s^#N*+ z6rk#)0f4`c1ANck{`ggU`{ysBx=S9KQMbVt18?*+_mP z@jGNRAL_{1-L|Xs14ar#{Er{ zJDi`oy%MPiBObUenUqGJ=a1#(0@X2-jh(U?NnESKPkTQ|Qr^cV9+abcLJJA#Dp#nw zk1@+=$;C&$o}EN%X02hmh-iiFB6MZkbA|`HepJIq&VL@t^Iyc7<6%)rJ#_M+m}OKq z3CpgGf%(*{Jsy?8!}=*GSW;&$a}|>H_vILvPT_KbtNw72kN9=J{o#`Gy{Zfe(o>2r z=-XX|>513F1{Kk~j~)8Z?FR?Ion;^(hd(rW7W3Ok2-!UtP4x7TM7S@3M?9rG0iZ+B zz2D|ScKeKA)ba6f5FbGMW!s{4s}5CFN(&j9WHm6*2I%=(*qMlx0ipkjIC-kOy)!0s zo%jU<_T|&}%p5?yokk6@#$z+bBv%{lZOQho(8SJ^rf7>K9mZvH0nUnHro*Njl6|(| z1j6Y0Ud$&$7gQPrMTbEBwcU+Lx66>paA<=eO=7z%S^q$i|KJEt;k76m%f|?3<3f zlmv~=M$mKL3`xrz*8Ej>a>LG1_=|RR_@@@X_9B7F##M>$&XD%EVj(KV3AMF%&Kxf{ zLn(v}Op#&qwN7m^-*tkoX}CQLYhd|JC($0|{&NuXW8Gku6lZZXzfC4j5h)o6Gpr3r zG>?Js;LCeLwVE4Q2v>81wagkVQ2=AeroS{PnsN8Ub#+M)6i>8xwn9UQ%~Uc10(7mj z!&kvvJhB=!dGh2=cQ%GCQwoDxd-7UTi&NckSvIw#Ie>R4-|(ZY0sWqw^MBDhlFC7N zvaU50v~E=lW{vqEn~o{HOtO1&N~QuQzpP0%!fWe1@0pn%RSRI%$|JwmefTWLa2V8> zJL3kAh@!rdGS{p#Sy7U+Dcg=j&Nnl(=_gxBCvD!<&II2^tF@WwT?$Kem2Z3D{6L}R z*V+{(sXNs$nfS@v-5NcTg!{TEU_5{X6+xijsE# z5KT2=GMiiIVc&HpWmd^J|ugOzRIOwh9Uu52AZxMi?z zD$&G>bzmJEYeDiFoR+o>DeHr%(Sb}tW(Sko6g`$vY2iFL=u<#pp6vGSrFKC9MS3}U zdW5+-=XE(PBK6_OuuXmTSrq&=e6XbEbq=K_*}~Z4Ss=074qdCG^S@$@ zqG_9MFF*v|p4j)pk{YP5ze6}KYWd)z9s;;k7B-;p18#zU@>X-v_OOXfWqYCz*n}8R zEpEaSD&WlbL#TdKSwTuMkPQ4cw;BU1N&*Ok?UFp@!gO>pr($g}-@V|^p4fb|M(^Q4 zRSs5+8K(Vedp2oH@Bzi5h_Ac-$d)ZIqe34fp=VQ=q^V#4WAlD6Y>Sh2Bpyca%G_e( z7c?T)oN%ka3UUa+%p}Sx7d*uDM#B3rR0mWsgCYYAiU-!$##C|Rk86O*Ekk$=JrX!* zcYT*|vPa`g&&gYa1HL+7JC|wtAGA^N+LSb43am}A22gT_i*v>S2hs-?ou;%%P4*tw z71~N~1U_ltUQccj0b+#It)&H(s?C^1>s&7%T=SH^A@4NoMS;Q(c4Ld%cau$S{T@@2!>F(-)!eYUU}rIt zhplnW+gZRFPqVuAPXJ*aCR<61y%*@ffrohQ@tMW)d|Hg1WD_J_@`fOWu&5}NyZb^j zJEkXdv_H=lwrpf{*E|;G-1Hgcn|cugx=vQFsWuQ)OMKTiBZb)e){)s%@te@YwlW$k>-%KNLQ?5mhNU0zEfacP>d=|S7dsHuP>Lts6h(JT^t_{jet=QXKiz9 zOHiQnDiN-flhAQvCe&3@2NjP+39(!Dh+bzRZAcesAlR+QNZmuM<(!ZKTD^#$*+F(buqH!D5_w7 zRxut|Rd&;d9wm};3oV1wes9<_0p~%u`D$9Q-QVRS82u}Z;h~+T)*-!K@|vz-y=`D( zD_FY~9H`MFU&G!SzwgJJcmc9vx3vvZ#ak>Dwy%z3dESwba;}OBoEUf{XL^SXqw>FF zNhNdOW5Ln$JeRAhNzZp9*f#Q+mD*})n251J5&;1B=V3u;^0ywmU<2+wdK&cx$wqu8 z$UEOY)izMFJSo8^!}ix+g2pdYn{<7Z^^9-Y=5Zm1v)Vdcxz>2evQ7|i$b8}}kMwVJ z2c|h~@khB=xj%oT`;Rztq>jX7zB`|Q+_*T!ZHK1l_|EGbdXl36&>lTPO)a(5fFx$T z)i^Sw!=_zQbfUBqQFWBzJdpQ8IvZG>L&_D??9d^=(Y>z~$VCMTfZN6&*%QsblEiZ# zA!j2IM;0o$>ZUyCv>=)?gQk4Ww3}M?ll-9@=qCwfkV-JmfOfjg=i_ohm6m(U=HlP2 zYi%2|_RePo$_TD-En5Zf0?9wzjJKNfP;o~?2VeOZ4c^!tt(Dl<%MGh>p)V07-TKJ8 zl)~^ruH?+HYCLNxu$gsq$Z7(o1DVWh4Eo=S3}g^NqZ;(1FU7R5(2w)Tai zfr_Ou9PSu{c+nOV?h^#ERk18?S`}8$ca1VPG^pam7#?jI6|)hn@;5$SCjMEmJGZx` z7WLu-%lsWQ7Go-Ngx zs5fu$s+gf&cWErdBy&=3Gy;~TTrvVnZ~AM*Q9((lf$%bQmUzU#w?1Yyd~`h-1!wJ6 zOVBpEvTK*C6vEO}PCqeATRQoO2|?>1JcAQOMj_yT{>#2Mgl9&IB$xFXM=I#)^szD0 z4h~k;4)_Gt!fKry(Oke%)znDUE8U=FP%bw@Ea&atLi&W9%BT;MF@ z>oCR%$2<*NkEexg2!DsK>#PD|;Rf{g*YMpjvqow-&P=L=6L@ygn8cMQRK|*5!C7sK zpb}c1=S_x=p!9?SDB%#b&4ZrgAsU2$GY7s&R*e{9;6O*-2MmupfBz-5knKPX*T z_^#t1yvtgBW`YawvDZAF3Psa~bHW4i&_At5;1Kz`@39zeA9^?p{=G4tiMzmdC?P$X z9Do&*iXw^V%@v^>GN$3je0oFQauz3(9a9Dr)yKiJtLqGyI(UT(5tGEa8TUh<&~SKt z`Y|iWx7ubNHC15YZ(F!C(6*dH8J0C>LPhtH6C(>BVh(uchK2Tf*d`L-$kn&uz!Wbs zN*K@PYjJe29*15J1i2Ir0CaHNAegXq(KLY=?t_wZ#wvE?}jOz404`7~ncw5d37!aO{New7~ z07!k+pS9?{c5P>+M~7_K_;c%djbg0X2e6V1J{C=`F?Z}YpO%+s5skU`CDIpV{T{hg z=f0FnHWVWd&hqA}q(E7W8#Upi&em7dCctV{$#ItM0_Xau@7eQg!5uNAq^V%66w0U( zF7CNosn<1A!*MXV(D{R%o8O4m5{Vtub z!^dRFl^5ZB<;8fEkhAm<`x{^hYLk?kGR}!_)rH9*11BDq10T@vAUqFqo($;z7nyX`{$tzoUa|RoJg6|1mA*?~`ZCspF(+JzZ9dTOmP`N03WTWjv#ieDNuW}DVY_s1_r>1+;oKd;Jw?)_YWUF+`0vf0C7)h=c}?B zoTH=XZ76yuXaS3w!Ld=@^fv73e#XQZ}fpnE&PzIz4#4fAs1!0!33$u6C3m z|30Wa{nP&OyZz(iqhkw=h=^_I^!Vmx^a)@N1lxRqSq~x&ZqM*$l$-cDHY`mmU8rw_ z$y2`eFefe?D}3n6oo|cq$%V?cQiQHSX928Is+ax|HKN9M|7#D-w@-98X5e3c-FQ&7 zy(b06#_DKC+~gtx$NUUho184p>*&9I3C)qD_E@#G!|`%*gbI1eSBHB+;ACZ_APg+E z3npKi-kywNDm9&0EAG7Q=bc7$Hp6x;k_Vcr0p`^nv7T7sDc6?lMlo>@*}v=0uI5ND zIV@-JB~iEsmCF!OYpl2oH0EYsZlAH$j~^c{u|O zW0HS*8+(s`v11*uC#k69IfbuVE36gY2DXc6m2~LnSJU1K@o}ZI_zIa+Af&%Nj~-_$ z*dufPw6Ix%OyURZF*Grl+to0@R+^iXl`KxI-jg%2Lydg1RIB!trk9atkQ%TWgjPbt z`x74P#6EbVee6k`PY=sWFRu5q?tT!F?#Ws07SO1{AWV|KmvLu7>)ie)Pz39KypOHW z`-&1|ai%Zj6qB)gPrQ$Txae$I=bRw7l@n7-}oO^IkT8^QPCd$(EvhQA!aZTl(!O1nTk9_r<*HD)fs7 zg@)tllJe-_Fr6?IT@YZUTQe5xHw#Pv18PW_Q>h;ypD7toIK4F-#KpuGM6-RI>>eLH zOV|`hpW@0I#WOpvF&_xsmd-*)cllTawiEM{N6AItxJz0 zx;0GYQRo)qnUtg5|Jq@Ee$+Cv1t0omG}O3ys83FNa`wtCBFe6?`Hsmx(iy2Gjn_}I z{iXQ2uFohXwA7PsmSjL)$r zkyJ7KhOD~EA_7%v8a@i_7@SRNTFx&y zmx4>oiMR1q5ca3a*=Xc|Su=2WKdNQxcKe7~qH0hRjeA3@#bmeiv?*yfN_D|Lrk9+0 zb+)B*J_SnTLVc5v#|AcrTA(j`SYynD_S7w;AJ zn8&CHwr+>}Bw#TuzblQs)(vmVyXZ3wg?=m)BREJ*ely3>S9bWw?+!Z?3DZWSNY`98 z6`zth)BHXB_Omol>5`2lm#fhF1%1inb35YCaV-0tZ*}y)jLbxCvO7tp%5yUO)+29! z4S9R>rDNuVRlC1roATPz1`J{{tbP*j(Gl3dZ7Q!nN)wf#f9XZ>$#$YxMe8$5^f6-k zLP9ck2|tG$1Ncra`9s&)XcmqA&FrYY{P0BVat38j=*;dgO>gq4KWem_Zcqbx&5qY? zUjF8r2xOj2D{t@?o68jyB&>&5lY!9d3t8;=+*XsQFBhu?a$O&&4midH_jy8Dl!GZ+af(G%Bu(vx7H)D+ zF>miH^=bNyhG}ox7>Td$>0oYh;%vkO(l!ED_iuN?>fPQwxlSSCj3vl3)O;3s)!eYp zKkkR~TyF`kOjtC=7rmAc8mtIm{J>GvlE^{sc3DXd&4RlsJt5oQq@n;&;G8eZ z21(@6F_(^~%U!?A;a`FCRg3ZF{?o>>m)3C;|6!?A)lLrPzLE?M{``cE{mh3-=xV4d z-kP4Do0JziE%d=8T$(PQ@r^yqKesbT+b5@4{occe%2s`6C-bX(3M|0^B){3bOtw>A zhNDGO8>7)wYK1Y{ABm|n$cP(eTI3vTm9Fw^>ix>zU?mNmjqAD`7i7sW9vov!Y8+L} zwsHpTrF_03s+0C0aH2)JhkwZz26E|n)TZl4(dmY^;dt+Z)m;yQeggp%I|6*ufAy`j z>l5GlfI3Gw4zQlO^^wC?MfmE#GWuJ6YicQ!i$%CtY3d521^~4LP&bpbZYIV2n-mi( z#oy*t^$na-fYb6u8@izTn6Y|W%DBkB-AFB zAlT4!AxC0eu+d5Cr&@%`a?whRPJF;bdQFyGQ z42}ZK)o-GZJUFz>aUrJv{KhzOz6A=YFql0&=B$6ZSj`u~!4vI?y9!Q4+tZf543usG z6&5(Qj}eox=M`HQB}u z)1y*NxWw~GR!vMYh5e7PfcrDd8(1Zzs@h`dH^7-lWf^D2SS8nu(yMiu0CGbG5vdWV z##jw#c-c`)aLQL;Lq*?M7DB%OZ7g}?31+K#Tanh*RUppT zJ_WwC1Qs2us~nwMYxt-|!!4M+yP18yX#rVDvYFzwLk@_U*Ty0fNM2H3&-RexmafT} z={y%hEBRb5IPODY50RIp8aSC$3M2N{h*h&}(Kl&PKo*lf*Oc<&A6r|VG$z#*Lt6JX zyQ{3|?LMW<#PqrW{GLxl2jZhxC(@^@EWQmn&nlm8+AGwth~ETiSO_5Lp8 zsx6~M9N&#Bo9$MYt|l%YU$%a6#o5%4vzaF$d|(G}jCOl|?hL3FAa96_`+waV4x1|! z(XkS!?hTZ|Q+}b`#oppuIEecKq1)r3_=LHq_ptouH)wum9@q;mhe{h*CMF>!{bS!X!UmUUqdxFM))Ro|cO# zEG>E(qSZgzFRF_sTPb|#aixwS{=74Utvhd~#rf#3lk3F}%z9qqe0L_zySf~|&znIu znIO&Yke~KdSvRBq{qXyT17-|6H-mavV~WO|`2bL#H#;|N@Jux|F#;o?i|l&towSKB z+@f|i^+Kdf9!<@cT>akHJyGxwVbo+OOqiGoR&`aq9ofE>FVmNg(dexO3ZeO`ob>9*+KNnTnRCgn8l6Ie4Ikp~v9K$q`ye!Cj(Vw&zx361&BR7Z(A8b>1YCaKG|HI2cay&@(;eU@F z_wjacq&+Zx@Vyg$c==d0A8-lJoHYiy8jM96Ir068S=Y`BAE#&>KVG%8rUQRz+uVhh z)I7HmKDFk6>RrcL2?41I9J8~t+a|1E6yr53GT;WGpK#(uy%#Pp{YBdA=Fi$tp$FGj z=xU8=RvIc%PPg=xO`Jx%6YyS#LSVK-{@YY7>)qupAUC7h$M@ix0pMg%*Rj#UuSJ zAj{vxXRx8HgvV^ZnD~{~9De z9UlF-d-$&X;_&Dv_j8q3-~Zp?4DqCcfD`jx^*AFi{5F;X!J<+?Cg9Z9%|~z zfQ8tM-#C3`=coCOe=iFV0L*cB@UfQ?TIOQ6x=*NB{Yi1y@oeur&w42wwlBwn{VV~t zaT#`{QzGdv>5OpZ{!Q{;qAoB~xZyJcrSKu z_)pRnkn1))roZUvp2pXLR9*6;Cf3g+0>XKp}^#=L0ACL1v`iO(bL+|ER z!H@D{cdTSJfOA*(jKJ$>inJgMGVtnGMHQf9({J!}fvjpp|841Ip%wzhmcitbYl^x3 zg0hdjy4CqsnjRoxxcu=-%I^{P?Y+|$za-RK;L+~$X@WaosVFK*aEr1Y2T)l};!nFCqh*+N}73 zXL8YJ=tVXEliuWGr^NK(+n~?Mf{dtSJ}SiHgxuR`T0Nf>M%YKZPm!9|-ETx{>RUjD zrm0>6ngoOvcH{XX{O<|7kC@Yz91QiUoOE=Q9=JkbfDZP$&(S;Lv^L1AZ#BwBCNbcCWTYAEtCC1}r)rCJ5iW@(|H?izj= z$>A9T(kn>oq%Ohwp?YZW95ag*$cOvd2d!E(5Rq419h7((M@KnV3hua^7MGT(D~TB_ z$231&hTY=={!uhAOH8I)Fx6P$%anF1rK5QuZX&^^adLVJ`7GR8Yl~>N)=(1hqDtnJ zX1%kYDb0=&RfJwInF-V>`yy@WyOyCPvRk7en#|QjDj=swo`|&@hbmFi;SfJaES$+T zMd~{NmzWdUS`JAdls6H80TZU73UJU0b4{(QiT$7dkrE!O2NmzHVEnDyAARE=5!PoF z(`;MkUwK+tHE8SHz$r@mb+lMCn%=iOh5Gwv;75)pV_nD8zYe)?7-utC=zyViUhS@a zgteP;%+*~3Y32O-$&0b*~_e6$`QMW^*GZZU};0TEiL-#>pZEi zRV{{6q=q^m*rbCTw1v=hNADjw0yZFc93(w8HOU!##tlpP7`}a_{`1;4X;g`&=x!_} zQS|4zyC2KP_hU1ZfThN*&$G)^%fWgSP`NdXa*0>)#cR(#@ISo%*rqbQ zAmG!82!JFBW~VbfJ&ntg3L}x|j2(x>&`#7*IF7tn3eGX+JS`)8-)LlLyeh(t@g@2) z4s8%sG2RixuPC~k0&e`sNR_xJXsy4zm-&CDrHYr)gbCCF%`rJ&OOoNd6S7QzACe+)vNv32tUfOZ>SRAev8E|e*yp6yup}%< ziJH9CHf_3pNzDU|7a7Q8MV+irpjpz4p9b9|vi*Si6ACQ=2bcj{HrYLZ0dIKEaW5D{ zbe?LfZi*=y+t1k|P|Bm*okc%qlZVqTMbypm=@`H&c`e8!fQfB5yBeJcLkRX%B;UhZ zD(KGCYl!I_gF=HF0W$dY2!7nJHJ$F9PbQ<%?(FgE?aX8)J)R>a}O*Z&uf(j@dD9vuJDP_;?CepL_&rH zkEx6~s5zi;j(qpFj;CB((ry?H-67I7Ajo zp#GKc79@B2V&_l4Z$vk9dew}i88<%FM2A}0x{nRZG>Dy6P$Y0c-I6E)J7smDtx+st zPs^Q2m#XU%!cwFCpqTKF&9H7QJWr*L`F9q1ZrXzji*-J+f&>LUxw?P?5W7Ls2Y^8o zDM*Xr#=$mOBTk8`)SVuUix~f>4&H*J3zO&~@2lB18Yf7IbuM66Bk<$zO$P}Ys@0)& zp+job=+9W=f|)HP%PKYl2NrBIr+V?7Q71az>1nh1b5d4X0Hqbvu1-rP7cajggG>Jz zRz5|=)9e~HzMX5qfm|osU-WXEH6zG+wx9Q=pr$=i=ky^>q=E8iqG~h$FbC2DmIBhM ztHZe|f5SEy!+|{3k9PaXXkz^aa3G4;MnThl{(v)*xG&saN7HOVN;s26koAkWED1H& zN=BKN+|fi+{g#Zua?g$@VJn+128|w%)AT6>>Y7inv59)#8v0Nt*TY(Q`3St!l)PjW z;{hr`c~IP%X6Rh2zHnhq9^11Gr=>bh(Kz@3lh>&SyS<4LKH#T65T3QAt)Gk{K;n~L zB*oSOD;)h1Twi`{){`IL-*=s30Cb5IO(XudakQt^g45v?1MvZ#e2|_JEOZ`$N}50y z2I?A#%IQ3RizopoV4P0D*ajB|iLd9tdYs3Sc_|cC@7jBK4m&p02kb1qzKDy(unu@~ za*(Bxr}Pur0$8XKJj_cU*Waka+gl#b#Y0W+E&JS+=@#-MOjv`10wGQ>AE9uq0e@g3 zl_-rYKL}F*jT?%V$uv)_`d$7(y)0R@gaC>TWW4*qA$fP@e&j<|eWvH#UanHr6XInZ zVQQ6Y=PP4s;s*#s#+M7IOtIl%Ixs=dF4l?mdvs%>k8vu2R~=3#qv>Ryfk%6(4rmF! zT)yt42RaI>pr(C`%`=NgX(DuW?g}B9hGOYQa-ZS zd(1n;yy|r57anV-h&kp;1$6g%`Dj)*1)EOG;3%0ZF&kj&7>6#b9`){0p?&%PYpSJ? z5+YR@_1_r4YU_4g*H|4YkdrLhsS;9cxcNxOZ$3~T79M6GzJ=Rn$FkVisis(m7XmtP zhE$GSsS+mZCpF(W(W&XOW=z|N{-Y!ruyT=dFG?>)6ULx+boYjC(IobudeSf5v4w_a zR>U2cy^&>)}j}XCr_SBCJXLu`&qYBa)1Fo4fwyp^B15o=PRUHbp7CxH5uL-nrnUwdF^rESMJwZG*`vNEVG~5d= zu@tbsr~~6-q;HFdLv`nQ`waYM-*K@A5bDB53d|}?5T<4)~!NqoEH{ewPIZMgyP37d_&Kbb`Cf1Klr zEZ}PTc1k0>IF=*ArfzD6o?Q;@q%=Z;$L*2V_{ttx;6!)cuZ$EOC-lINiJ-fPclF?~3s+qXY&f zvaW@0@L6fv7+bPQ1J`NvtkF)2bZ|-u<0l@7q-1k*&1ct6{vK-OuS}2@24GZ+ zp^g&Y_1R0N*6+e$Xj5R7kF*?$50gUp4jh5n^i|OS_GQ0Y^v+Y;g9Ys+idgssI~?3>t_ zn-p;R;G_HfeGMU|CH#hB+-SA6M>XJMviowIJ0 zi9B%qh-dp z1R2ar%aYjhK^}rv<&Tp0Nj3oU$CJSmB_j%kTUedqr>`J7zO=ju9(MtM zLR0lm(R7@3j6w+Cpi;?lFGO!5)eXNN9QmtmXjbdb^~3qOW}hn3JVx;pCb6ASV8&O|=fYkIr}R_19gCGj@wqkrK%!fdzNCP6D;3f+aC_uB2$#n;NT0eHtXg>pKxl= z{Y*(UC=->MK`=gCBSdyDS24`@sK-A8LmbTgpksf>%cA@zIJSz(aB>wmL*OrNA4|PoHhi>Rahr#B;L75$x#dc zZ}0HQvqz72j~^XAjrI?p9`NH@n48w6%lBig`c50x{e7#HHF~90f!I%XxSRov6_FDQxb^yB8J zVRHfnBo6Rg#qv9ZjLQQ`&&eI_FWMdw7G8umh#cOcjIKcVvvgt#s4AM=3KnE|F6w7CE<|7Bwx?0>tu)V}j+Lp9Y+?JXKb{5!zrWyN`YNv2WNE?l;;G>NjxEQ(|$NT+|)D5W$x*KtP;g2WqA- z{s*WCRPXDJGQhFjAIDY7>!=e*UGflfleaJemfHU4Ug$7P_3-bXcp#U7R4eiX<_ZAr z4o^=}UBif=cA_+c*!tld7QJr1cOwbmD6Sf)K!;C+`%fJC>tMAI@ zdI#Biq$Uf{>^o6=ZOytt@c(88qfk+{>lj}xA~HJ-f*-J&Q#116Zi-OI;rwesyi2&MstDr!Xd7vHGv^X z!=}Nqqydt1VR439p~=H!{wnP;3LqeC*!l6jr$tO=%sJM_ODnL}xCUYE5ctpYNAs96uPj;x4xYT2pnA+1S8WfywBoJMNKrW5gJUD#(`0%$kj~_k#<*0!^eQKBcw3`nVK~9cy z%Ii{>X0?|_#bF7QBYE%y>65fHAF|=bdaWUDX#6SkP7O?L3~8Yek8^c)K^gtX!9{_Q zf$Q89;?Sp~Q9ACSDsT+vjbc#~z?}mK$sPc5W$G%VF-NrYLiMV7c?sqB5(}Tfj|57qbFH zAk`%M{-STHQ=?=q&I@iBQiYr)W55$F1SOKY#J|KX_SYi}snmXmR#$=GWL{z_4QH_; z6@-;)+Ua{f0rV6YvfgJCb>h?@8g4j&d>BZXYtFO=e3km(oiwaUyJE+O%;TX%Xj<6E z%~VV?(yvl1ZKtMg9WDvE6ztKwbN(`aiGEHwcVoe>xd1RqRy>fw*)@ui#ibbh5Qmrs z2w7Z#SOM$wdB={Mh{h0Jp-x#0vj`naQu;?LYFhUVOMwpy{`L7jbJ#sj zNlLTlesh2te)qGKwfm5Wo7VxY*=sHG#Z>Fj=072o6vsIarWZrLUeh(jb=VW~>r}83 z9!j4hPGb39giuOarFtA=o8L!bL8q4KRqtG#6g`L%HSg7E9!ps@TE95Ltmzuy1E&%> z+MEN4_m1no3h~*QXiG&sN+|q{+X>9YO9ZN=0j#&hy44vmacE_db%6&EUJ2wM4}F;p z&9@lb?^udBZzlWZ^uf{k>jNfyiBjfnW56b;m;LAt*e7<@kHJ6OzdYkq)3O>2gKC->hgMo za|#iO;oB#*^@%LKvIL52>hs-CG|3KMh2NOnmPnn}7@8y@x+&PslL7|kQUe}KHnd^2 zE<$q@3b$j$=EDS11QzGX`qp->Ay`U|)PUZg(oUnH1QsCwdhI;@XzbN@EtBCcqim1V zc{XY4xCRhiK@CU&5|BI7Z2zKTn;dPvz&oCdY-am>NNb!SWyZVONL*a>@N~bcmj-5U ziB)h??}qxp&b!z;OAbvhzC#@C)kp8sh$#V*0&vFgiq1aHyxx+I-f{mgch8*Vwihi_xYDkn)5~FyVSA(rK8R?<6h{ z4@>QRictRLO1U!th2I58{SOfJbDj)sFh|;(wEaK}tfWtbanxz%Fr4?^Pg~*TgQ|f&8(=tA3mI`RY?1mYMK>BM0RCi%hMmhWfTk#1b>G>b97-< zvSSK)Z_{B9NDAl=jhJ9^u`>^78WTy6WWZ$b0dQpVp;%$*ETE&_*fxUALyxHo0Epyt zgTza>;n+w+va?tost44~75R^!NeAi;Mt{F=2qfl{4XUM=oh4)~5t;}^PuB;GzFF=X1Y@rxMAKEh@{*~aj zgT~C6wb6~H&&i-?@LOXq+@L}M%Q!2DBXE!VNWm(YDD9PE)%1(Gf??6G#bwIhfE4Ot zhEFOUxIh_!#FpmBNz-9Mu^j76yMd*;YpRBQ0L`aE&O)VbWJ=sbgClP}rfhJJI~#DA zq)>-_&koO>&+LI|jacP@NYIek1cciYxp|+!N|0QoCLP}O1xOhW0$U|3W$?(;Ke$Tk z>#^8x0Ut_nxdT6T$eVOmzHZJ3v8dYUV~FdZ7%W(fAEw~bY*?IA&wswO%K zS42=&UOmD|?nN~cMR7NVb_$MM4d=uCn%|dm)zXmrQi9sC@C(Eb#)#(Jyd|uEBAi(1 zuJwkOwrMR;lc%?7&4(@M%ilaWL(|+#d3H zo$jq(iuoj;CqKEk^EBCGTa2S?@zS&oLwa$ z8JB8@A%)4hD7n#Voy1xNE?HL3erO+>FlLR+t6a#2^g+a zixfz>N|SjP{AFP`ps~iv8f~^1)s6P^X(2xUl!vZFbD7@%x<1CMi=HKDY>thHB5PFP z^HiP98^$J+5=X4qpa%j$Ll;u`7t^QJs?62aH2cP+DNnPDNeLa7LNV)SKyO|Hx~a`| z^dP@I33$&^&dmtoJj*B6{XCTIS+`5KzPomhc;~p;Q%vS&u+J6XDO9h;?*^CzFRFN{ z-p=zcuZ7tUU)>X({zFa2T?mt5+4)>Jutw;Me*adr_WQSR+Tfq+@7Rq6b60D@kZz#U z@3efjT-W{X+G-W#t^of7!h$(_!ut&70N*4LLuo0wZ8&Xmb?lg4Bm#Le@9dJ}#%Vxm z!t=N4fCM%Go~p5s6?d-W&>Yw*Op~~q^%6PMNOE7*{l}})UQ86qi~1ft04|@6wt-#rH z26|JhPDixX|L z`1>a=FE%e#?6MT6%*V557mH7PYsSy%#IsABqFx?t4uxea=m&K73wRr<&BxAvJwD+bRtx%pLkRo%0Nrt!zuw8mq zN1exy!{%7dMo%1)Txa=IR*3wR-wmJ(I(FQvLXsAZ(^HskfG;dH$`^SSVNPTiC#t?w zbrhg&%*Q;sLz*}6i}Cu*;Yai_*4Z&G$-MP#>Ju4 z>4=O6yh$yxB))_1f}XY^anF#xEpix`2L7u!|D5~N1Hq9nm$lgAguAk zGOo%V7YUJYkhJ{zVXw(~Z13rC1A z@gddDm1SgMt#ROGHcYMQjoI0iwIbU8N!T7j;X>&&>C5!$x4IU6cl2kzR>(kkjJ7>7 z5d2YPK-M(e37Ei&rXJ-*L0+g6R%{VRfBDE_K)QgOiUExCx;`nJ zQNV*o&Of-XErX$`ntJtGa~}UJuJ8|dK}WPyYnjemKr+95)PR^uZA4_wwx8WN%#)fyIG z1hxh@sk#kJjt3g-Bc+=NIb_Ar3*J_E{zu;%d&&tb|Mcy5W@?1t0S|X4 ztRhC7dmFliXs1&fhsP`ZO0Y7QT?8AZrVi0lF+MeQe2sfO7*H#IH&DvX5@#h0O zh*o=ZF2u~QzbkB=lIe_LKB`X;EA$DXmpWc*g(hm*PE!m9osRshMK;Q-@A4A%b$e%_ z_(Sz9AzN-W;tmsZd?~8{xXo%md7F_IS833!1?qK#J2^a%-WcyDq6~%-#AZ!-a# z8B}H2X}sK&AfS_s0Tlr>Y$kI^%%OxjA$-WvOLTiQ8!C=7$$CVMb)FtJ&}!qXVv3ch zihH|H_YNKtzS+D~IV4~@)@A<3QrqD?#)EWt#!A(-64&)StZ1$QoYXsfZv0?DKB@7;bMxoaya@E>F+9s6n zo~{yu>>`^`LW&~$U%D(5JxCKsJkv~sDh?TqQ-N+&5-!E1 z@7NsBMotMDI2*1gP}D_&h{A%dG<0Xx{4oE3H{cK8NrtLTMFo~7SoqNd1#B7xUDR{Q zjtuWEK$`fF%yiWO5JL-DhWVMk6b5DGOMM{-_?8D9|1LWxwnq(zwW4Q_S|}#yLezss zmX!ilTq`NwA!i4Uf9{r~uq%5&7?-?+`z^H~F_PK@aoH)o0!4A) zF@@^|UT~aV_zi{VT{cdvE|;d0vc&4gCAQ)cLEyVfn`1h%8zl!qZwqxkm0M=!{=>a! z-48n^znU>!J1=rn>nFo#-CdTVNj}%8DPo0#&iSoKT|EUH0ZIt&)Q|ollGkROU0j$L zMB~vxHHj=vEmP|9Xt&qP#}NB7m|>_BShk%8CKQZ<+fkQhU^^MtI;isoc=+Vv{DV6= z%qrCU$IJ2j+H>C&Wb={Jgy4WN61cP|boD`4qIqZdyyh#Z;C@u6AoIi-3nbbz%6piv0aR`y+1^L=1|>4r=gM~>hHEs`SOr)Q zktQU9M$t(OI*0;JV#NS#9+o;#JmoCRmY3WF;Wlu^oRmhuP7?<+PW?ZeHe@wJcvI;c+L`5O-rRG8|^0FQs;QG$+Cpq*zAe!xLNX0@nnXxte>0T85SfJ zpVf5iq%5NawbCHDb1z}pWmhi=TUp5nt8X9`K7jtLHSH;|Ea*4~j!8lVR-^c?=&)2D$FULu9G368 znvpAsGlAbUwTq8t8Tg0ns3tIqi)q2;P)oW6le($C{U7b*&>75baHyT z#^F(y4bcQME(Uw4SDs$r{2n-jwH4zcd8-JN-4|Fq5Fu1MH1UDvn6|(vafH&mT83tg z^o2D|;l>xx1Yj1%YsELR#OV5h@)lh|bPyZ%q#2#%(AKY+TK69#N=EHA+9PjLHdxcq zOIjDBz3-3nk-F^+V>iR$p5%!ueoLCBAdjyu_cxsY_CE_RbSKMQ-gPS5Mr;yup`M2zkQVaqlO24sw-v ztgsfLGN~};c%3ESoq!+wyJ*3$xha;HPp!OM{^lYZ0{g>n@NXw!F>VdTk?aG1k5>{& z^pTF)c%k<9W7Oe{S*&=BUUd82$IQ*hnuIUmrt*`L+Es1mdDc^cIVggHuV6#J91pxN z${|MZ-ENOg6`jX5z7>aa4^s#!VF`;TQ{)z_N;G_gGX|RZL`}*4Yn$g;`)%fKw z`w0A2b-TT_vvYf6`}W$JRwH>Q2ue3_wo-PK`xfE~ymA%v=DU!dT?ms3UL_TwKLo$!`buS(&UFpA*Eb$Ih6SF)VPIF;NPx&5Kv%?LBtSxZqzNn28pu{ zu)0*u*Cn=A#^=@FE8c!1t*qb7>iZl~79Eu3kcAwW=8^=UZmDY#pFsb;tA9h=d&SMY zzS{Dln6~t`j#BE>fA6BUKf{J@|2Zqp854}-tSwhBy-#;nThrZ}T-4optLk_3r`0<8 z(@l2trz?`*?u|(eKV4gF*sVS}g5UqN;G_xO@BXpSE$e8UU8uXIC>c6Ytv4Fg0?T?@P}&#JBx6es+;dDA z(xCy@)U8H(p%u^NEnRV;#xb%-jn3R#!4Wt@5LCcoJ|Zovo&=0=7xgflBYqW zGY-Y7liFeBvkZaZi?2C?@}yVx(CVnwO$?Bt7*#c^h8!rkth6<`PgM#zb+72Q{++x} z$aRXl)lFjya%PDt)ZDa^R=;b?n|xk+5IrTc6hYMiLQkWbwJ7Oh%|I-zY{ym7D)z9! zoh_za?F9yDaYe@tX-}ql9C zQ&b4+Z*{*JH=!m@OG&ZYE%L#1LKt>$B%s7g)xeS7n#fAAb5AG_6SyYmX*B4HTANi+ zzx-aD*`4-Z4)Qbqksc1=RDkO@<$$Q^2Ge;ud27Dr>hAUY6{uz}ta6Ux-F!#CtGnfl zwE8nO<~c)?(RaW}kI1`3+;fL-Q~KNwdBH(V42j=|6E8{LI2{cX&DP(%=dwoo_XbQ^ zHXgNsrM(UXA42pI2`^F}fl1~Na^=2Btlsxqts;;!3qGg$^H0(z=pFwA>)fl~dAcDa+W z|B@5OAn2X?3Tq-mKMNq>wuJ3~TPI^KjTyd1S4PF^Cwgz3H0Xjm(aA_uFRaGd?0|?G zV0-Fw71{~g^h%eyr8h0uLR5Wbi`d^Qaz0k8a~w|jr)C&psGxWwx^JEVE5K(Rr!wDg zVNXAtEvcS;919Hs{bkg={qyMXX^Xm;oz4(z`$}u{-d>zIdh+t4*-}zPrKX#G7`Z^Q!J&)V_L< z)6MV=Y%=3+UfS%mq;q@V$PM;(c3*t5jsk0HfPxgY!=n9AM&J4IW(2gtoi zerYL@;|&xwK)f&~s@LO#v(hQWqi_o+AJOu=zNt1O|Fh|Qfw)yilN4BY-TDxms^#~# zQSeCq6wt_XDYy1@&Ft$z*9F9mm^JG5u+&xa$qXc@)9fQSRwo~uFfG=(aSOm*Bh1b! zxqT#0$T;n#8KR`;fcVc&*?b~djl}s7_#|jH>T8WS8J`ue+OO-MB8n#HSTmt@F)Nj~ zCdIn6^Mw?K1Gi1+ats2~UOpX8>KjH_FMkHfabrRSM6@=&^h(4Y*-l?-!jA9}%$$BG z{%B@yqgQM3TJ+>6@E+d8zh2jx&L#dz9nlkag$}jj?LVm3gjw%8Y zu8tu~jX z!&C)tSS9KLPDc<;nOdfS%SMbrJM|Q50&lx|+PIlX9p+DRr`$-Q=7O zF~-H7O9WFN8(dLz-&$cxIRGf>nIM7H=eAuvxAVM5X_nR2oy}%n9{tLs*t4DBH3;WU z;;vSibPaCDu>#kg{JMb@{R>y%Pb2p*3qj3B5>3a0tFmZ7r`PcvP|xH;W-=KK<%64I z;R}nluU>ThN?gwI^wvbcDWp1TM!vl5xDqWaf3hq~3fDM!{u)Zy{L3lfGqVL)vh-s% zVLz_kfHF{=U`!Vrn;(#T>Dm)BO_MI6Cv2Bfvut{Rb6 zia>+kx8xq)A|7DB$Jm>eSE@SJsub8}g%R!RX*k4L@FJ8Tln*AYJY7z#Q$<9b9gI|J z2>1y-OdnEy4*O~iq(BZ|Qmk$HS-6o)>T*na61Twmss%PyEwH(20UEk7>!ZUWdfocd zW4>X%(!#(i_;x|qmKSmr*|Uoozsy){Rg~63S#!JE=NqeizPadgzW|dI+c0PTWa{yT z=Xz1OYX-Dg=Qs~3nv9d)TS$?u_5h&q0AvBSSTZ&EV)fbZoLIX^`3ga6F!E7Q^LMwR)cTc5Z7eFK^`yK&*(ALvw2_r_{$!9)5>oDg z4Y?0d;z$siesy`nf^qEy# zD<2i5%`Ytj38#~X8PM@8z$Mm~{cxlD!y6%wYF+ap5?Qh2#&RX+ZFYp2o8%+f^@yXh zoo5dbA=Ub};pUfA3XK&$g{-GXx{rXwZ1`F>|16-D%#uKwS0#uB*XzcOlz@pK@n z1PIodq%Dj9(b@$1K=cV~&SOu7kj<&aR!Wq05^N5yQc3Ld0dRpnSKpS3i|WN2n|d=k zpA}gTkbFbR8TCOjo@A#z|3RPuI7)Nq1wTx3iIK-RXTL>@SkA0rVDp&aY3_>(?dgt*i6$_SV*yz{|H! z`~7yef9G^|*4J*g z+nZl${40&~>@P**+p80~6urIqCGhy2jowCgy}z;D?Qh>+|0>pene>k%ZSn2@2FQN% znq{jH^-j~xwe_!j{fngiNeWS?lkw~ZNPhd8B;Q`YbM*^rX}|m> zM8COpr=6T`C)@4Y?f&VVuMGbSCHi90n%;!IedqIwF}mpOFM;5 z+8bZF{1-{^L+gyZv7OD`|g;q7IRDb81RNvlOn@{!Y^Cg(I&qVh9WMgN$ce>r(xU<&VS--MAvc2<_?*AmZze>c1 z)Vi8w^}7B9?FF~k1vJ0)nP@(_ecDUf{dT*%neKFxJ6ERpJ6~!3E6w}j%a=*>-EOEyJ%4LMU^4l}(>s-aw6#wJm{M`)HK*F!34IQzYZ;v^7Xzy{TzvST*~%%fI~}P)i30@esw4psN4?F6sgR zP)h*<6ay3h2mlBGgo#R3@esw4psN4?F6sgR4FDhj0000000000q=5hc0044jb9HQV zb1rjXa%pB&R1E+JSwuu*T|`7 Date: Mon, 10 Oct 2022 18:37:52 +0100 Subject: [PATCH 5/8] Make download method handle zip files This will: - download a zip file as an ArrayBuffer - save the file as `results.zip` - unzip the contents into a `results/` folder For the tests: - In order to check whether we're saving the correct files in the tests, we've had to make the `getRepoStorageDirectory` method public. Unfortunately the temporary file path generated for tests is random so we're not able to hardcode it. - Now that we have a real zip file to use in our tests, we're first converting this file into an ArrayBuffer, then stubbing the API to return it. We then check that it's saved and unzipped correctly. --- .../variant-analysis-results-manager.ts | 14 ++++-- .../variant-analysis-results-manager.test.ts | 47 +++++++++++++++++-- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/extensions/ql-vscode/src/remote-queries/variant-analysis-results-manager.ts b/extensions/ql-vscode/src/remote-queries/variant-analysis-results-manager.ts index 3a13a223e..b4e2374fe 100644 --- a/extensions/ql-vscode/src/remote-queries/variant-analysis-results-manager.ts +++ b/extensions/ql-vscode/src/remote-queries/variant-analysis-results-manager.ts @@ -14,6 +14,7 @@ import { DisposableObject, DisposeHandler } from '../pure/disposable-object'; import { VariantAnalysisRepoTask } from './gh-api/variant-analysis'; import * as ghApiClient from './gh-api/gh-api-client'; import { EventEmitter } from 'vscode'; +import { unzipFile } from '../pure/zip'; type CacheKey = `${number}/${string}`; @@ -58,8 +59,15 @@ export class VariantAnalysisResultsManager extends DisposableObject { repoTask.artifact_url ); - fs.mkdirSync(resultDirectory, { recursive: true }); - await fs.writeFile(path.join(resultDirectory, 'results.zip'), JSON.stringify(result, null, 2), 'utf8'); + if (!(await fs.pathExists(resultDirectory))) { + await fs.mkdir(resultDirectory, { recursive: true }); + } + + const zipFilePath = path.join(resultDirectory, 'results.zip'); + const unzippedFilesDirectory = path.join(resultDirectory, 'results'); + + fs.writeFileSync(zipFilePath, Buffer.from(result)); + await unzipFile(zipFilePath, unzippedFilesDirectory); this._onResultDownloaded.fire({ variantAnalysisId, @@ -156,7 +164,7 @@ export class VariantAnalysisResultsManager extends DisposableObject { ); } - private getRepoStorageDirectory(variantAnalysisId: number, fullName: string): string { + public getRepoStorageDirectory(variantAnalysisId: number, fullName: string): string { return path.join( this.getStorageDirectory(variantAnalysisId), fullName diff --git a/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-results-manager.test.ts b/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-results-manager.test.ts index f61186de4..b7d584b0a 100644 --- a/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-results-manager.test.ts +++ b/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-results-manager.test.ts @@ -5,6 +5,7 @@ import { CodeQLExtensionInterface } from '../../../extension'; import { logger } from '../../../logging'; import { Credentials } from '../../../authentication'; import * as fs from 'fs-extra'; +import * as path from 'path'; import { VariantAnalysisResultsManager } from '../../../remote-queries/variant-analysis-results-manager'; import { createMockVariantAnalysisRepoTask } from '../../factories/remote-queries/gh-api/variant-analysis-repo-task'; @@ -12,6 +13,7 @@ import { CodeQLCliServer } from '../../../cli'; import { storagePath } from '../global.helper'; import { faker } from '@faker-js/faker'; import * as ghApiClient from '../../../remote-queries/gh-api/gh-api-client'; +import { VariantAnalysisRepoTask } from '../../../remote-queries/gh-api/variant-analysis'; describe(VariantAnalysisResultsManager.name, () => { let sandbox: sinon.SinonSandbox; @@ -69,12 +71,29 @@ describe(VariantAnalysisResultsManager.name, () => { }); describe('when the artifact_url is present', async () => { - it('should save the result to disk', async () => { - const dummyRepoTask = createMockVariantAnalysisRepoTask(); + let dummyRepoTask: VariantAnalysisRepoTask; + let storageDirectory: string; + let arrayBuffer: ArrayBuffer; - const dummyResult = 'this-is-a-repo-result'; - getVariantAnalysisRepoResultStub = sandbox.stub(ghApiClient, 'getVariantAnalysisRepoResult').withArgs(mockCredentials, dummyRepoTask.artifact_url as string).resolves(dummyResult); + beforeEach(async () => { + dummyRepoTask = createMockVariantAnalysisRepoTask(); + storageDirectory = variantAnalysisResultsManager.getRepoStorageDirectory(variantAnalysisId, dummyRepoTask.repository.full_name); + const sourceFilePath = path.join(__dirname, '../../../../src/vscode-tests/cli-integration/data/variant-analysis-results.zip'); + arrayBuffer = fs.readFileSync(sourceFilePath).buffer; + + getVariantAnalysisRepoResultStub = sandbox + .stub(ghApiClient, 'getVariantAnalysisRepoResult') + .withArgs(mockCredentials, dummyRepoTask.artifact_url as string) + .resolves(arrayBuffer); + }); + + afterEach(async () => { + fs.removeSync(`${storageDirectory}/results.zip`); + fs.removeSync(`${storageDirectory}/results`); + }); + + it('should call the API to download the results', async () => { await variantAnalysisResultsManager.download( mockCredentials, variantAnalysisId, @@ -83,6 +102,26 @@ describe(VariantAnalysisResultsManager.name, () => { expect(getVariantAnalysisRepoResultStub.calledOnce).to.be.true; }); + + it('should save the results zip file to disk', async () => { + await variantAnalysisResultsManager.download( + mockCredentials, + variantAnalysisId, + dummyRepoTask + ); + + expect(fs.existsSync(`${storageDirectory}/results.zip`)).to.be.true; + }); + + it('should unzip the results in a `results/` folder', async () => { + await variantAnalysisResultsManager.download( + mockCredentials, + variantAnalysisId, + dummyRepoTask + ); + + expect(fs.existsSync(`${storageDirectory}/results/results.sarif`)).to.be.true; + }); }); }); }); From c400485a4e033ae7fc1ea0664a38e73375c84b14 Mon Sep 17 00:00:00 2001 From: Elena Tanasoiu Date: Mon, 10 Oct 2022 23:03:26 +0100 Subject: [PATCH 6/8] Delete duplicate test This checks the same thing as the test before it. --- .../remote-queries/variant-analysis-manager.test.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-manager.test.ts b/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-manager.test.ts index 678c4d300..6393c3fb1 100644 --- a/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-manager.test.ts +++ b/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-manager.test.ts @@ -143,16 +143,6 @@ describe('Variant Analysis Manager', async function() { expect(getVariantAnalysisRepoResultStub.calledOnce).to.be.true; }); - - it('should save the result to disk', async () => { - await variantAnalysisManager.autoDownloadVariantAnalysisResult( - scannedRepos[0], - variantAnalysis, - cancellationTokenSource.token - ); - - expect(getVariantAnalysisRepoResultStub.calledOnce).to.be.true; - }); }); }); }); From 39025968234fbd368934193a1181aaf82f89d552 Mon Sep 17 00:00:00 2001 From: Elena Tanasoiu Date: Mon, 10 Oct 2022 23:04:01 +0100 Subject: [PATCH 7/8] Use real zip file for VariantAnalysisManager download tests Now that we're unzipping results, we also have to use something closer to a zip file when testing download functionality for the `variantAnalysisManager`. The `variantAnalysisManager` has access to the `variantAnalysisResultsManager` so we could've stubbed the result manager's `download` method instead of going as far as using a zip fixture. However, since the results manager is private it seems bad to make it public in order to stub one of its methods. So using realistic data in the setup seems like a good compromise. --- .../variant-analysis-manager.test.ts | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-manager.test.ts b/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-manager.test.ts index 6393c3fb1..11b91eb15 100644 --- a/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-manager.test.ts +++ b/extensions/ql-vscode/src/vscode-tests/cli-integration/remote-queries/variant-analysis-manager.test.ts @@ -7,10 +7,12 @@ import * as config from '../../../config'; import * as ghApiClient from '../../../remote-queries/gh-api/gh-api-client'; import { Credentials } from '../../../authentication'; import * as fs from 'fs-extra'; +import * as path from 'path'; import { VariantAnalysisManager } from '../../../remote-queries/variant-analysis-manager'; import { VariantAnalysis as VariantAnalysisApiResponse, + VariantAnalysisRepoTask, VariantAnalysisScannedRepository as ApiVariantAnalysisScannedRepository } from '../../../remote-queries/gh-api/variant-analysis'; import { createMockApiResponse } from '../../factories/remote-queries/gh-api/variant-analysis-api-response'; @@ -72,6 +74,7 @@ describe('Variant Analysis Manager', async function() { describe('when credentials are valid', async () => { let getOctokitStub: sinon.SinonStub; + let arrayBuffer: ArrayBuffer; beforeEach(async () => { const mockCredentials = { @@ -80,16 +83,18 @@ describe('Variant Analysis Manager', async function() { }) } as unknown as Credentials; sandbox.stub(Credentials, 'initialize').resolves(mockCredentials); + + const sourceFilePath = path.join(__dirname, '../../../../src/vscode-tests/cli-integration/data/variant-analysis-results.zip'); + arrayBuffer = fs.readFileSync(sourceFilePath).buffer; }); describe('when the artifact_url is missing', async () => { beforeEach(async () => { const dummyRepoTask = createMockVariantAnalysisRepoTask(); delete dummyRepoTask.artifact_url; - getVariantAnalysisRepoStub = sandbox.stub(ghApiClient, 'getVariantAnalysisRepo').resolves(dummyRepoTask); - const dummyResult = new ArrayBuffer(24); - getVariantAnalysisRepoResultStub = sandbox.stub(ghApiClient, 'getVariantAnalysisRepoResult').resolves(dummyResult); + getVariantAnalysisRepoStub = sandbox.stub(ghApiClient, 'getVariantAnalysisRepo').resolves(dummyRepoTask); + getVariantAnalysisRepoResultStub = sandbox.stub(ghApiClient, 'getVariantAnalysisRepoResult').resolves(arrayBuffer); }); it('should not try to download the result', async () => { @@ -104,12 +109,13 @@ describe('Variant Analysis Manager', async function() { }); describe('when the artifact_url is present', async () => { - beforeEach(async () => { - const dummyRepoTask = createMockVariantAnalysisRepoTask(); - getVariantAnalysisRepoStub = sandbox.stub(ghApiClient, 'getVariantAnalysisRepo').resolves(dummyRepoTask); + let dummyRepoTask: VariantAnalysisRepoTask; - const dummyResult = new ArrayBuffer(24); - getVariantAnalysisRepoResultStub = sandbox.stub(ghApiClient, 'getVariantAnalysisRepoResult').resolves(dummyResult); + beforeEach(async () => { + dummyRepoTask = createMockVariantAnalysisRepoTask(); + + getVariantAnalysisRepoStub = sandbox.stub(ghApiClient, 'getVariantAnalysisRepo').resolves(dummyRepoTask); + getVariantAnalysisRepoResultStub = sandbox.stub(ghApiClient, 'getVariantAnalysisRepoResult').resolves(arrayBuffer); }); it('should return early if variant analysis is cancelled', async () => { From 95cbe027685d1bbee0a7dbeb830e63abacfe2ed9 Mon Sep 17 00:00:00 2001 From: Koen Vlaswinkel Date: Tue, 11 Oct 2022 11:10:45 +0200 Subject: [PATCH 8/8] Use unzipped file path for loading results --- .../remote-queries/variant-analysis-results-manager.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/extensions/ql-vscode/src/remote-queries/variant-analysis-results-manager.ts b/extensions/ql-vscode/src/remote-queries/variant-analysis-results-manager.ts index 94911ba33..e9ec3e6ea 100644 --- a/extensions/ql-vscode/src/remote-queries/variant-analysis-results-manager.ts +++ b/extensions/ql-vscode/src/remote-queries/variant-analysis-results-manager.ts @@ -27,6 +27,7 @@ export type ResultDownloadedEvent = { export class VariantAnalysisResultsManager extends DisposableObject { private static readonly REPO_TASK_FILENAME = 'repo_task.json'; + private static readonly RESULTS_DIRECTORY = 'results'; private readonly cachedResults: Map; @@ -68,7 +69,7 @@ export class VariantAnalysisResultsManager extends DisposableObject { await fs.outputJson(path.join(resultDirectory, VariantAnalysisResultsManager.REPO_TASK_FILENAME), repoTask); const zipFilePath = path.join(resultDirectory, 'results.zip'); - const unzippedFilesDirectory = path.join(resultDirectory, 'results'); + const unzippedFilesDirectory = path.join(resultDirectory, VariantAnalysisResultsManager.RESULTS_DIRECTORY); fs.writeFileSync(zipFilePath, Buffer.from(result)); await unzipFile(zipFilePath, unzippedFilesDirectory); @@ -116,8 +117,9 @@ export class VariantAnalysisResultsManager extends DisposableObject { const fileLinkPrefix = this.createGitHubDotcomFileLinkPrefix(repoTask.repository.full_name, repoTask.database_commit_sha); - const sarifPath = path.join(storageDirectory, 'results.sarif'); - const bqrsPath = path.join(storageDirectory, 'results.bqrs'); + const resultsDirectory = path.join(storageDirectory, VariantAnalysisResultsManager.RESULTS_DIRECTORY); + const sarifPath = path.join(resultsDirectory, 'results.sarif'); + const bqrsPath = path.join(resultsDirectory, 'results.bqrs'); if (await fs.pathExists(sarifPath)) { const interpretedResults = await this.readSarifResults(sarifPath, fileLinkPrefix);