From 0f534f5829b2e991ed7d67169d882659f921a60d Mon Sep 17 00:00:00 2001 From: Ivan <98037481+IvanZosimov@users.noreply.github.com> Date: Mon, 15 May 2023 14:09:29 +0200 Subject: [PATCH] Refactor and update unit-tests (#418) * Update unit-tests and dotnet-install scripts --- .github/workflows/e2e-tests.yml | 2 +- __tests__/__snapshots__/authutil.test.ts.snap | 22 +- __tests__/authutil.test.ts | 56 +- __tests__/csc.test.ts | 42 +- __tests__/installation-scripts.test.ts | 52 ++ __tests__/installer.test.ts | 629 ++++++++++-------- __tests__/setup-dotnet.test.ts | 204 +++--- externals/install-dotnet.ps1 | 459 ++++++------- externals/install-dotnet.sh | 14 +- 9 files changed, 853 insertions(+), 627 deletions(-) create mode 100644 __tests__/installation-scripts.test.ts diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 9963bbc..3ec658b 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -24,7 +24,7 @@ jobs: - name: Clear toolcache shell: pwsh run: __tests__/clear-toolcache.ps1 ${{ runner.os }} - - name: Setup dotnet 2.2.402 and 3.1.404 + - name: Setup dotnet 2.2.402, 3.1.404 and 3.0.x uses: ./ with: dotnet-version: | diff --git a/__tests__/__snapshots__/authutil.test.ts.snap b/__tests__/__snapshots__/authutil.test.ts.snap index d310f14..0ea506b 100644 --- a/__tests__/__snapshots__/authutil.test.ts.snap +++ b/__tests__/__snapshots__/authutil.test.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`authutil tests Existing config not in repo root, sets up a partial NuGet.config user/PAT for GPR 1`] = ` +exports[`authutil tests existing config not in repo root, sets up a partial NuGet.config user/PAT for GPR 1`] = ` " @@ -15,7 +15,7 @@ exports[`authutil tests Existing config not in repo root, sets up a partial NuGe " `; -exports[`authutil tests Existing config w/ Azure Artifacts source and NuGet.org, sets up a partial NuGet.config user/PAT for GPR 1`] = ` +exports[`authutil tests existing config w/ Azure Artifacts source and NuGet.org, sets up a partial NuGet.config user/PAT for GPR 1`] = ` " @@ -30,7 +30,7 @@ exports[`authutil tests Existing config w/ Azure Artifacts source and NuGet.org, " `; -exports[`authutil tests Existing config w/ GPR source and NuGet.org, sets up a partial NuGet.config user/PAT for GPR 1`] = ` +exports[`authutil tests existing config w/ GPR source and NuGet.org, sets up a partial NuGet.config user/PAT for GPR 1`] = ` " @@ -45,7 +45,7 @@ exports[`authutil tests Existing config w/ GPR source and NuGet.org, sets up a p " `; -exports[`authutil tests Existing config w/ no GPR sources, sets up a full NuGet.config with URL and user/PAT for GPR 1`] = ` +exports[`authutil tests existing config w/ no GPR sources, sets up a full NuGet.config with URL and user/PAT for GPR 1`] = ` " @@ -63,7 +63,7 @@ exports[`authutil tests Existing config w/ no GPR sources, sets up a full NuGet. " `; -exports[`authutil tests Existing config w/ no sources, sets up a full NuGet.config with URL and user/PAT for GPR 1`] = ` +exports[`authutil tests existing config w/ no sources, sets up a full NuGet.config with URL and user/PAT for GPR 1`] = ` " @@ -81,7 +81,7 @@ exports[`authutil tests Existing config w/ no sources, sets up a full NuGet.conf " `; -exports[`authutil tests Existing config w/ only Azure Artifacts source, sets up a partial NuGet.config user/PAT for GPR 1`] = ` +exports[`authutil tests existing config w/ only Azure Artifacts source, sets up a partial NuGet.config user/PAT for GPR 1`] = ` " @@ -96,7 +96,7 @@ exports[`authutil tests Existing config w/ only Azure Artifacts source, sets up " `; -exports[`authutil tests Existing config w/ only GPR source, sets up a partial NuGet.config user/PAT for GPR 1`] = ` +exports[`authutil tests existing config w/ only GPR source, sets up a partial NuGet.config user/PAT for GPR 1`] = ` " @@ -111,7 +111,7 @@ exports[`authutil tests Existing config w/ only GPR source, sets up a partial Nu " `; -exports[`authutil tests Existing config w/ two GPR sources, sets up a partial NuGet.config user/PAT for GPR 1`] = ` +exports[`authutil tests existing config w/ two GPR sources, sets up a partial NuGet.config user/PAT for GPR 1`] = ` " @@ -130,7 +130,7 @@ exports[`authutil tests Existing config w/ two GPR sources, sets up a partial Nu " `; -exports[`authutil tests No existing config, sets up a full NuGet.config with URL and other owner/PAT for GPR 1`] = ` +exports[`authutil tests no existing config, sets up a full NuGet.config with URL and other owner/PAT for GPR 1`] = ` " @@ -148,7 +148,7 @@ exports[`authutil tests No existing config, sets up a full NuGet.config with URL " `; -exports[`authutil tests No existing config, sets up a full NuGet.config with URL and token for other source 1`] = ` +exports[`authutil tests no existing config, sets up a full NuGet.config with URL and token for other source 1`] = ` " @@ -166,7 +166,7 @@ exports[`authutil tests No existing config, sets up a full NuGet.config with URL " `; -exports[`authutil tests No existing config, sets up a full NuGet.config with URL and user/PAT for GPR 1`] = ` +exports[`authutil tests no existing config, sets up a full NuGet.config with URL and user/PAT for GPR 1`] = ` " diff --git a/__tests__/authutil.test.ts b/__tests__/authutil.test.ts index d9a0b7a..3435cdf 100644 --- a/__tests__/authutil.test.ts +++ b/__tests__/authutil.test.ts @@ -91,9 +91,9 @@ describe('authutil tests', () => { process.env['NUGET_AUTH_TOKEN'] = ''; }); - it('No existing config, sets up a full NuGet.config with URL and user/PAT for GPR', async () => { + it('no existing config, sets up a full NuGet.config with URL and user/PAT for GPR', async () => { process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN'; - await auth.configAuthentication( + auth.configAuthentication( 'https://nuget.pkg.github.com/OwnerName/index.json', '', fakeSourcesDirForTesting @@ -104,10 +104,10 @@ describe('authutil tests', () => { ).toMatchSnapshot(); }); - it('No existing config, auth token environment variable not provided, throws', async () => { + it('no existing config, auth token environment variable not provided, throws', async () => { let thrown = false; try { - await auth.configAuthentication( + auth.configAuthentication( 'https://nuget.pkg.github.com/OwnerName/index.json', '', fakeSourcesDirForTesting @@ -118,10 +118,10 @@ describe('authutil tests', () => { expect(thrown).toBe(true); }); - it('No existing config, sets up a full NuGet.config with URL and other owner/PAT for GPR', async () => { + it('no existing config, sets up a full NuGet.config with URL and other owner/PAT for GPR', async () => { process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN'; process.env['INPUT_OWNER'] = 'otherorg'; - await auth.configAuthentication( + auth.configAuthentication( 'https://nuget.pkg.github.com/otherorg/index.json', '', fakeSourcesDirForTesting @@ -132,7 +132,7 @@ describe('authutil tests', () => { ).toMatchSnapshot(); }); - it('Existing config (invalid), tries to parse an invalid NuGet.config and throws', async () => { + it('existing config (invalid), tries to parse an invalid NuGet.config and throws', async () => { process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN'; const inputNuGetConfigPath: string = path.join( fakeSourcesDirForTesting, @@ -141,7 +141,7 @@ describe('authutil tests', () => { fs.writeFileSync(inputNuGetConfigPath, invalidNuGetConfig); let thrown = false; try { - await auth.configAuthentication( + auth.configAuthentication( 'https://nuget.pkg.github.com/OwnerName/index.json', '', fakeSourcesDirForTesting @@ -152,14 +152,14 @@ describe('authutil tests', () => { expect(thrown).toBe(true); }); - it('Existing config w/ no sources, sets up a full NuGet.config with URL and user/PAT for GPR', async () => { + it('existing config w/ no sources, sets up a full NuGet.config with URL and user/PAT for GPR', async () => { process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN'; const inputNuGetConfigPath: string = path.join( fakeSourcesDirForTesting, 'nuget.config' ); fs.writeFileSync(inputNuGetConfigPath, emptyNuGetConfig); - await auth.configAuthentication( + auth.configAuthentication( 'https://nuget.pkg.github.com/OwnerName/index.json', '', fakeSourcesDirForTesting @@ -170,14 +170,14 @@ describe('authutil tests', () => { ).toMatchSnapshot(); }); - it('Existing config w/ no GPR sources, sets up a full NuGet.config with URL and user/PAT for GPR', async () => { + it('existing config w/ no GPR sources, sets up a full NuGet.config with URL and user/PAT for GPR', async () => { process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN'; const inputNuGetConfigPath: string = path.join( fakeSourcesDirForTesting, 'nuget.config' ); fs.writeFileSync(inputNuGetConfigPath, nugetorgNuGetConfig); - await auth.configAuthentication( + auth.configAuthentication( 'https://nuget.pkg.github.com/OwnerName/index.json', '', fakeSourcesDirForTesting @@ -188,14 +188,14 @@ describe('authutil tests', () => { ).toMatchSnapshot(); }); - it('Existing config w/ only GPR source, sets up a partial NuGet.config user/PAT for GPR', async () => { + it('existing config w/ only GPR source, sets up a partial NuGet.config user/PAT for GPR', async () => { process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN'; const inputNuGetConfigPath: string = path.join( fakeSourcesDirForTesting, 'nuget.config' ); fs.writeFileSync(inputNuGetConfigPath, gprNuGetConfig); - await auth.configAuthentication( + auth.configAuthentication( 'https://nuget.pkg.github.com/OwnerName/index.json', '', fakeSourcesDirForTesting @@ -206,14 +206,14 @@ describe('authutil tests', () => { ).toMatchSnapshot(); }); - it('Existing config w/ GPR source and NuGet.org, sets up a partial NuGet.config user/PAT for GPR', async () => { + it('existing config w/ GPR source and NuGet.org, sets up a partial NuGet.config user/PAT for GPR', async () => { process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN'; const inputNuGetConfigPath: string = path.join( fakeSourcesDirForTesting, 'nuget.config' ); fs.writeFileSync(inputNuGetConfigPath, gprnugetorgNuGetConfig); - await auth.configAuthentication( + auth.configAuthentication( 'https://nuget.pkg.github.com/OwnerName/index.json', '', fakeSourcesDirForTesting @@ -224,14 +224,14 @@ describe('authutil tests', () => { ).toMatchSnapshot(); }); - it('Existing config w/ two GPR sources, sets up a partial NuGet.config user/PAT for GPR', async () => { + it('existing config w/ two GPR sources, sets up a partial NuGet.config user/PAT for GPR', async () => { process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN'; const inputNuGetConfigPath: string = path.join( fakeSourcesDirForTesting, 'nuget.config' ); fs.writeFileSync(inputNuGetConfigPath, twogprNuGetConfig); - await auth.configAuthentication( + auth.configAuthentication( 'https://nuget.pkg.github.com', '', fakeSourcesDirForTesting @@ -242,7 +242,7 @@ describe('authutil tests', () => { ).toMatchSnapshot(); }); - it('Existing config w/ spaces in key, throws for now', async () => { + it('existing config w/ spaces in key, throws for now', async () => { process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN'; const inputNuGetConfigPath: string = path.join( fakeSourcesDirForTesting, @@ -251,7 +251,7 @@ describe('authutil tests', () => { fs.writeFileSync(inputNuGetConfigPath, spaceNuGetConfig); let thrown = false; try { - await auth.configAuthentication( + auth.configAuthentication( 'https://nuget.pkg.github.com/OwnerName/index.json', '', fakeSourcesDirForTesting @@ -262,7 +262,7 @@ describe('authutil tests', () => { expect(thrown).toBe(true); }); - it('Existing config not in repo root, sets up a partial NuGet.config user/PAT for GPR', async () => { + it('existing config not in repo root, sets up a partial NuGet.config user/PAT for GPR', async () => { process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN'; const inputNuGetConfigDirectory: string = path.join( fakeSourcesDirForTesting, @@ -274,7 +274,7 @@ describe('authutil tests', () => { ); fs.mkdirSync(inputNuGetConfigDirectory, {recursive: true}); fs.writeFileSync(inputNuGetConfigPath, gprNuGetConfig); - await auth.configAuthentication( + auth.configAuthentication( 'https://nuget.pkg.github.com/OwnerName/index.json', 'subfolder/nuget.config', fakeSourcesDirForTesting @@ -285,14 +285,14 @@ describe('authutil tests', () => { ).toMatchSnapshot(); }); - it('Existing config w/ only Azure Artifacts source, sets up a partial NuGet.config user/PAT for GPR', async () => { + it('existing config w/ only Azure Artifacts source, sets up a partial NuGet.config user/PAT for GPR', async () => { process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN'; const inputNuGetConfigPath: string = path.join( fakeSourcesDirForTesting, 'nuget.config' ); fs.writeFileSync(inputNuGetConfigPath, azureartifactsNuGetConfig); - await auth.configAuthentication( + auth.configAuthentication( 'https://pkgs.dev.azure.com/amullans/_packaging/GitHubBuilds/nuget/v3/index.json', '', fakeSourcesDirForTesting @@ -303,14 +303,14 @@ describe('authutil tests', () => { ).toMatchSnapshot(); }); - it('Existing config w/ Azure Artifacts source and NuGet.org, sets up a partial NuGet.config user/PAT for GPR', async () => { + it('existing config w/ Azure Artifacts source and NuGet.org, sets up a partial NuGet.config user/PAT for GPR', async () => { process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN'; const inputNuGetConfigPath: string = path.join( fakeSourcesDirForTesting, 'nuget.config' ); fs.writeFileSync(inputNuGetConfigPath, azureartifactsnugetorgNuGetConfig); - await auth.configAuthentication( + auth.configAuthentication( 'https://pkgs.dev.azure.com/amullans/_packaging/GitHubBuilds/nuget/v3/index.json', '', fakeSourcesDirForTesting @@ -321,9 +321,9 @@ describe('authutil tests', () => { ).toMatchSnapshot(); }); - it('No existing config, sets up a full NuGet.config with URL and token for other source', async () => { + it('no existing config, sets up a full NuGet.config with URL and token for other source', async () => { process.env['NUGET_AUTH_TOKEN'] = 'TEST_FAKE_AUTH_TOKEN'; - await auth.configAuthentication( + auth.configAuthentication( 'https://pkgs.dev.azure.com/amullans/_packaging/GitHubBuilds/nuget/v3/index.json', '', fakeSourcesDirForTesting diff --git a/__tests__/csc.test.ts b/__tests__/csc.test.ts index 85433b7..8d43b39 100644 --- a/__tests__/csc.test.ts +++ b/__tests__/csc.test.ts @@ -1,21 +1,45 @@ import cscFile from '../.github/csc.json'; describe('csc tests', () => { - it('Valid regular expression', async () => { - const regex = cscFile['problemMatcher'][0]['pattern'][0]['regexp']; + test('regular expression in csc.json is valid', async () => { + const regexPattern = cscFile['problemMatcher'][0]['pattern'][0]['regexp']; + const regexResultsMap = cscFile['problemMatcher'][0]['pattern'][0]; - console.log(regex); - const re = new RegExp(regex); + const regex = new RegExp(regexPattern); - // Ideally we would verify that this const stringsToMatch = [ 'Program.cs(10,79): error CS1002: ; expected [/Users/zacharyeisinger/Documents/repo/setup-dotnet/__tests__/sample-broken-csproj/sample.csproj]', "S:\\Msbuild\\src\\Build\\Evaluation\\ExpressionShredder.cs(33,7): error CS1003: Syntax error, ',' expected [S:\\msbuild\\src\\Build\\Microsoft.Build.csproj > Properties:prop]" ]; + // Expected results are calculated according to the csc matcher located in csc.json file + const expectedResults = [ + { + file: 'Program.cs', + line: '10', + severity: 'error', + code: 'CS1002', + message: '; expected', + fromPath: + '/Users/zacharyeisinger/Documents/repo/setup-dotnet/__tests__/sample-broken-csproj/sample.csproj' + }, + { + file: 'S:\\Msbuild\\src\\Build\\Evaluation\\ExpressionShredder.cs', + line: '33', + severity: 'error', + code: 'CS1003', + message: "Syntax error, ',' expected", + fromPath: + 'S:\\msbuild\\src\\Build\\Microsoft.Build.csproj > Properties:prop' + } + ]; - stringsToMatch.forEach(string => { - const matchStr = string.match(re); - console.log(matchStr); - expect(matchStr).toEqual(expect.anything()); + stringsToMatch.map((string, index) => { + const matchedResultsArray = string.match(regex); + for (const propName in expectedResults[index]) { + const propertyIndex = regexResultsMap[propName]; + const expectedPropValue = expectedResults[index][propName]; + const matchedPropValue = matchedResultsArray![propertyIndex]; + expect(matchedPropValue).toEqual(expectedPropValue); + } }); }, 10000); }); diff --git a/__tests__/installation-scripts.test.ts b/__tests__/installation-scripts.test.ts new file mode 100644 index 0000000..e309a98 --- /dev/null +++ b/__tests__/installation-scripts.test.ts @@ -0,0 +1,52 @@ +import path from 'path'; +import fs from 'fs'; +import * as hc from '@actions/http-client'; + +describe('Dotnet installation scripts tests', () => { + it('Uses an up to date bash download script', async () => { + const httpCallbackClient = new hc.HttpClient('setup-dotnet-test', [], { + allowRetries: true, + maxRetries: 3 + }); + const response: hc.HttpClientResponse = await httpCallbackClient.get( + 'https://dot.net/v1/dotnet-install.sh' + ); + expect(response.message.statusCode).toBe(200); + const upToDateContents: string = await response.readBody(); + const currentContents: string = fs + .readFileSync( + path.join(__dirname, '..', 'externals', 'install-dotnet.sh') + ) + .toString(); + expect(normalizeFileContents(currentContents)).toBe( + normalizeFileContents(upToDateContents) + ); + }, 30000); + + it('Uses an up to date powershell download script', async () => { + const httpCallbackClient = new hc.HttpClient('setup-dotnet-test', [], { + allowRetries: true, + maxRetries: 3 + }); + const response: hc.HttpClientResponse = await httpCallbackClient.get( + 'https://dot.net/v1/dotnet-install.ps1' + ); + expect(response.message.statusCode).toBe(200); + const upToDateContents: string = await response.readBody(); + const currentContents: string = fs + .readFileSync( + path.join(__dirname, '..', 'externals', 'install-dotnet.ps1') + ) + .toString(); + expect(normalizeFileContents(currentContents)).toBe( + normalizeFileContents(upToDateContents) + ); + }, 30000); +}); + +function normalizeFileContents(contents: string): string { + return contents + .trim() + .replace(new RegExp('\r\n', 'g'), '\n') + .replace(new RegExp('\r', 'g'), '\n'); +} diff --git a/__tests__/installer.test.ts b/__tests__/installer.test.ts index 1a7e024..b26d915 100644 --- a/__tests__/installer.test.ts +++ b/__tests__/installer.test.ts @@ -1,298 +1,399 @@ -import * as io from '@actions/io'; -import * as os from 'os'; -import fs from 'fs'; -import path from 'path'; import each from 'jest-each'; -import * as hc from '@actions/http-client'; +import semver from 'semver'; +import * as exec from '@actions/exec'; +import * as core from '@actions/core'; +import * as io from '@actions/io'; import * as installer from '../src/installer'; -import {QualityOptions} from '../src/setup-dotnet'; import {IS_WINDOWS} from '../src/utils'; -import {IS_LINUX} from '../src/utils'; +import {QualityOptions} from '../src/setup-dotnet'; -let toolDir: string; +describe('installer tests', () => { + const env = process.env; -if (IS_WINDOWS) { - toolDir = path.join(process.env['PROGRAMFILES'] + '', 'dotnet'); -} else if (IS_LINUX) { - toolDir = '/usr/share/dotnet'; -} else { - toolDir = path.join(process.env['HOME'] + '', '.dotnet'); -} -const tempDir = path.join(__dirname, 'runner', 'temp'); + beforeEach(() => { + jest.resetModules(); + process.env = {...env}; + }); -process.env['RUNNER_TOOL_CACHE'] = toolDir; -process.env['RUNNER_TEMP'] = tempDir; + describe('DotnetCoreInstaller tests', () => { + const getExecOutputSpy = jest.spyOn(exec, 'getExecOutput'); + const warningSpy = jest.spyOn(core, 'warning'); + const whichSpy = jest.spyOn(io, 'which'); + const maxSatisfyingSpy = jest.spyOn(semver, 'maxSatisfying'); -describe('DotnetCoreInstaller tests', () => { - beforeAll(async () => { - process.env.RUNNER_TOOL_CACHE = toolDir; - process.env.DOTNET_INSTALL_DIR = toolDir; - process.env.RUNNER_TEMP = tempDir; - process.env.DOTNET_ROOT = ''; - try { - await io.rmRF(`${toolDir}/*`); - await io.rmRF(`${tempDir}/*`); - } catch (err) { - console.log( - `Failed to remove test directories, check the error message:${os.EOL}`, - err.message - ); - } - }, 30000); + describe('installDotnet() tests', () => { + whichSpy.mockImplementation(() => Promise.resolve('PathToShell')); - afterEach(async () => { - try { - await io.rmRF(`${toolDir}/*`); - await io.rmRF(`${tempDir}/*`); - } catch (err) { - console.log( - `Failed to remove test directories, check the error message:${os.EOL}`, - err.message - ); - } - }, 30000); + it('should throw the error in case of non-zero exit code of the installation script. The error message should contain logs.', async () => { + const inputVersion = '3.1.100'; + const inputQuality = '' as QualityOptions; + const errorMessage = 'fictitious error message!'; + getExecOutputSpy.mockImplementation(() => { + return Promise.resolve({ + exitCode: 1, + stdout: '', + stderr: errorMessage + }); + }); + const dotnetInstaller = new installer.DotnetCoreInstaller( + inputVersion, + inputQuality + ); + await expect(dotnetInstaller.installDotnet()).rejects.toThrow( + `Failed to install dotnet, exit code: 1. ${errorMessage}` + ); + }); - it('Aquires multiple versions of dotnet', async () => { - const versions = ['2.2.207', '3.1.120']; + it('should return version of .NET SDK after installation complete', async () => { + const inputVersion = '3.1.100'; + const inputQuality = '' as QualityOptions; + getExecOutputSpy.mockImplementation(() => { + return Promise.resolve({exitCode: 0, stdout: '', stderr: ''}); + }); + maxSatisfyingSpy.mockImplementation(() => inputVersion); - for (const version of versions) { - await getDotnet(version); - } - expect(fs.existsSync(path.join(toolDir, 'sdk', '2.2.207'))).toBe(true); - expect(fs.existsSync(path.join(toolDir, 'sdk', '3.1.120'))).toBe(true); + const dotnetInstaller = new installer.DotnetCoreInstaller( + inputVersion, + inputQuality + ); + const installedVersion = await dotnetInstaller.installDotnet(); - if (IS_WINDOWS) { - expect(fs.existsSync(path.join(toolDir, 'dotnet.exe'))).toBe(true); - } else { - expect(fs.existsSync(path.join(toolDir, 'dotnet'))).toBe(true); - } + expect(installedVersion).toBe(inputVersion); + }); - expect(process.env.DOTNET_ROOT).toBeDefined(); - expect(process.env.PATH).toBeDefined(); - expect(process.env.DOTNET_ROOT).toBe(toolDir); - expect(process.env.PATH?.startsWith(toolDir)).toBe(true); - }, 600000); + it(`should supply 'version' argument to the installation script if supplied version is in A.B.C syntax`, async () => { + const inputVersion = '6.0.300'; + const inputQuality = '' as QualityOptions; - it('Acquires version of dotnet if no matching version is installed', async () => { - await getDotnet('3.1.201'); - expect(fs.existsSync(path.join(toolDir, 'sdk', '3.1.201'))).toBe(true); - if (IS_WINDOWS) { - expect(fs.existsSync(path.join(toolDir, 'dotnet.exe'))).toBe(true); - } else { - expect(fs.existsSync(path.join(toolDir, 'dotnet'))).toBe(true); - } + getExecOutputSpy.mockImplementation(() => { + return Promise.resolve({exitCode: 0, stdout: '', stderr: ''}); + }); + maxSatisfyingSpy.mockImplementation(() => inputVersion); - expect(process.env.DOTNET_ROOT).toBeDefined(); - expect(process.env.PATH).toBeDefined(); - expect(process.env.DOTNET_ROOT).toBe(toolDir); - expect(process.env.PATH?.startsWith(toolDir)).toBe(true); - }, 600000); //This needs some time to download on "slower" internet connections + const dotnetInstaller = new installer.DotnetCoreInstaller( + inputVersion, + inputQuality + ); - it('Acquires generic version of dotnet if no matching version is installed', async () => { - await getDotnet('3.1'); - const directory = fs - .readdirSync(path.join(toolDir, 'sdk')) - .filter(fn => fn.startsWith('3.1.')); - expect(directory.length > 0).toBe(true); - if (IS_WINDOWS) { - expect(fs.existsSync(path.join(toolDir, 'dotnet.exe'))).toBe(true); - } else { - expect(fs.existsSync(path.join(toolDir, 'dotnet'))).toBe(true); - } + await dotnetInstaller.installDotnet(); - expect(process.env.DOTNET_ROOT).toBeDefined(); - expect(process.env.PATH).toBeDefined(); - expect(process.env.DOTNET_ROOT).toBe(toolDir); - expect(process.env.PATH?.startsWith(toolDir)).toBe(true); - }, 600000); //This needs some time to download on "slower" internet connections + const scriptArguments = ( + getExecOutputSpy.mock.calls[0][1] as string[] + ).join(' '); + const expectedArgument = IS_WINDOWS + ? `-Version ${inputVersion}` + : `--version ${inputVersion}`; - it('Returns string with installed SDK version', async () => { - const version = '3.1.120'; + expect(scriptArguments).toContain(expectedArgument); + }); - const installedVersion = await getDotnet(version); + it(`should warn if the 'quality' input is set and the supplied version is in A.B.C syntax`, async () => { + const inputVersion = '6.0.300'; + const inputQuality = 'ga' as QualityOptions; - expect(installedVersion).toBe('3.1.120'); - }, 600000); + getExecOutputSpy.mockImplementation(() => { + return Promise.resolve({exitCode: 0, stdout: '', stderr: ''}); + }); + maxSatisfyingSpy.mockImplementation(() => inputVersion); - it('Throws if no location contains correct dotnet version', async () => { - await expect(async () => { - await getDotnet('1000.0.0'); - }).rejects.toThrow(); - }, 30000); + const dotnetInstaller = new installer.DotnetCoreInstaller( + inputVersion, + inputQuality + ); - it('Uses an up to date bash download script', async () => { - const httpCallbackClient = new hc.HttpClient('setup-dotnet-test', [], { - allowRetries: true, - maxRetries: 3 - }); - const response: hc.HttpClientResponse = await httpCallbackClient.get( - 'https://dot.net/v1/dotnet-install.sh' - ); - expect(response.message.statusCode).toBe(200); - const upToDateContents: string = await response.readBody(); - const currentContents: string = fs - .readFileSync( - path.join(__dirname, '..', 'externals', 'install-dotnet.sh') - ) - .toString(); - expect(normalizeFileContents(currentContents)).toBe( - normalizeFileContents(upToDateContents) - ); - }, 30000); + await dotnetInstaller.installDotnet(); - it('Uses an up to date powershell download script', async () => { - const httpCallbackClient = new hc.HttpClient('setup-dotnet-test', [], { - allowRetries: true, - maxRetries: 3 - }); - const response: hc.HttpClientResponse = await httpCallbackClient.get( - 'https://dot.net/v1/dotnet-install.ps1' - ); - expect(response.message.statusCode).toBe(200); - const upToDateContents: string = await response.readBody(); - const currentContents: string = fs - .readFileSync( - path.join(__dirname, '..', 'externals', 'install-dotnet.ps1') - ) - .toString(); - expect(normalizeFileContents(currentContents)).toBe( - normalizeFileContents(upToDateContents) - ); - }, 30000); -}); + expect(warningSpy).toHaveBeenCalledWith( + `'dotnet-quality' input can be used only with .NET SDK version in A.B, A.B.x, A and A.x formats where the major tag is higher than 5. You specified: ${inputVersion}. 'dotnet-quality' input is ignored.` + ); + }); -describe('DotnetVersionResolver tests', () => { - each([ - '3.1', - '3.x', - '3.1.x', - '3.1.*', - '3.1.X', - '3.1.2', - '3.1.0-preview1' - ]).test( - "if valid version: '%s' is supplied, it should return version object with some value", - async version => { - const dotnetVersionResolver = new installer.DotnetVersionResolver( - version - ); - const versionObject = await dotnetVersionResolver.createDotNetVersion(); + it(`should warn if the 'quality' input is set and version isn't in A.B.C syntax but major tag is lower then 6`, async () => { + const inputVersion = '3.1'; + const inputQuality = 'ga' as QualityOptions; - expect(!!versionObject.value).toBe(true); - } - ); + getExecOutputSpy.mockImplementation(() => { + return Promise.resolve({exitCode: 0, stdout: '', stderr: ''}); + }); + maxSatisfyingSpy.mockImplementation(() => inputVersion); - each([ - '.', - '..', - ' . ', - '. ', - ' .', - ' . . ', - ' .. ', - ' . ', - '-1.-1', - '-1', - '-1.-1.-1', - '..3', - '1..3', - '1..', - '.2.3', - '.2.x', - '*.', - '1.2.', - '1.2.-abc', - 'a.b', - 'a.b.c', - 'a.b.c-preview', - ' 0 . 1 . 2 ', - 'invalid' - ]).test( - "if invalid version: '%s' is supplied, it should throw", - async version => { - const dotnetVersionResolver = new installer.DotnetVersionResolver( - version + const dotnetInstaller = new installer.DotnetCoreInstaller( + inputVersion, + inputQuality + ); + + await dotnetInstaller.installDotnet(); + + expect(warningSpy).toHaveBeenCalledWith( + `'dotnet-quality' input can be used only with .NET SDK version in A.B, A.B.x, A and A.x formats where the major tag is higher than 5. You specified: ${inputVersion}. 'dotnet-quality' input is ignored.` + ); + }); + + each(['6', '6.0', '6.0.x', '6.0.*', '6.0.X']).test( + `should supply 'quality' argument to the installation script if quality input is set and version (%s) is not in A.B.C syntax`, + async inputVersion => { + const inputQuality = 'ga' as QualityOptions; + const exitCode = 0; + getExecOutputSpy.mockImplementation(() => { + return Promise.resolve({ + exitCode: exitCode, + stdout: '', + stderr: '' + }); + }); + maxSatisfyingSpy.mockImplementation(() => inputVersion); + + const dotnetInstaller = new installer.DotnetCoreInstaller( + inputVersion, + inputQuality + ); + + await dotnetInstaller.installDotnet(); + + const scriptArguments = ( + getExecOutputSpy.mock.calls[0][1] as string[] + ).join(' '); + const expectedArgument = IS_WINDOWS + ? `-Quality ${inputQuality}` + : `--quality ${inputQuality}`; + + expect(scriptArguments).toContain(expectedArgument); + } ); - await expect( - async () => await dotnetVersionResolver.createDotNetVersion() - ).rejects.toThrow(); - } - ); + each(['6', '6.0', '6.0.x', '6.0.*', '6.0.X']).test( + `should supply 'channel' argument to the installation script if version (%s) isn't in A.B.C syntax`, + async inputVersion => { + const inputQuality = '' as QualityOptions; + const exitCode = 0; + getExecOutputSpy.mockImplementation(() => { + return Promise.resolve({ + exitCode: exitCode, + stdout: '', + stderr: '' + }); + }); + maxSatisfyingSpy.mockImplementation(() => inputVersion); - each(['3.1', '3.1.x', '3.1.*', '3.1.X']).test( - "if version: '%s' that can be resolved to 'channel' option is supplied, it should set type to 'channel' in version object", - async version => { - const dotnetVersionResolver = new installer.DotnetVersionResolver( - version + const dotnetInstaller = new installer.DotnetCoreInstaller( + inputVersion, + inputQuality + ); + + await dotnetInstaller.installDotnet(); + + const scriptArguments = ( + getExecOutputSpy.mock.calls[0][1] as string[] + ).join(' '); + const expectedArgument = IS_WINDOWS + ? `-Channel 6.0` + : `--channel 6.0`; + + expect(scriptArguments).toContain(expectedArgument); + } ); - const versionObject = await dotnetVersionResolver.createDotNetVersion(); - - expect(versionObject.type.toLowerCase().includes('channel')).toBe(true); - } - ); - - each(['6.0', '6.0.x', '6.0.*', '6.0.X']).test( - "if version: '%s' that can be resolved to 'channel' option is supplied and its major tag is >= 6, it should set type to 'channel' and qualityFlag to 'true' in version object", - async version => { - const dotnetVersionResolver = new installer.DotnetVersionResolver( - version - ); - const versionObject = await dotnetVersionResolver.createDotNetVersion(); - - expect(versionObject.type.toLowerCase().includes('channel')).toBe(true); - expect(versionObject.qualityFlag).toBe(true); - } - ); - - each(['3.1.2', '3.1.0-preview1']).test( - "if version: '%s' that can be resolved to 'version' option is supplied, it should set quality flag to 'false' and type to 'version' in version object", - async version => { - const dotnetVersionResolver = new installer.DotnetVersionResolver( - version - ); - const versionObject = await dotnetVersionResolver.createDotNetVersion(); - - expect(versionObject.type.toLowerCase().includes('version')).toBe(true); - expect(versionObject.qualityFlag).toBe(false); - } - ); - - each(['3.1.2', '3.1']).test( - 'it should create proper line arguments for powershell/bash installation scripts', - async version => { - const dotnetVersionResolver = new installer.DotnetVersionResolver( - version - ); - const versionObject = await dotnetVersionResolver.createDotNetVersion(); - const windowsRegEx = new RegExp(/^-[VC]/); - const nonWindowsRegEx = new RegExp(/^--[vc]/); if (IS_WINDOWS) { - expect(windowsRegEx.test(versionObject.type)).toBe(true); - expect(nonWindowsRegEx.test(versionObject.type)).toBe(false); - } else { - expect(nonWindowsRegEx.test(versionObject.type)).toBe(true); - expect(windowsRegEx.test(versionObject.type)).toBe(false); + it(`should supply '-ProxyAddress' argument to the installation script if env.variable 'https_proxy' is set`, async () => { + process.env['https_proxy'] = 'https://proxy.com'; + const inputVersion = '6.0.100'; + const inputQuality = '' as QualityOptions; + + getExecOutputSpy.mockImplementation(() => { + return Promise.resolve({exitCode: 0, stdout: '', stderr: ''}); + }); + maxSatisfyingSpy.mockImplementation(() => inputVersion); + + const dotnetInstaller = new installer.DotnetCoreInstaller( + inputVersion, + inputQuality + ); + + await dotnetInstaller.installDotnet(); + + const scriptArguments = ( + getExecOutputSpy.mock.calls[0][1] as string[] + ).join(' '); + + expect(scriptArguments).toContain( + `-ProxyAddress ${process.env['https_proxy']}` + ); + }); + + it(`should supply '-ProxyBypassList' argument to the installation script if env.variable 'no_proxy' is set`, async () => { + process.env['no_proxy'] = 'first.url,second.url'; + const inputVersion = '6.0.100'; + const inputQuality = '' as QualityOptions; + + getExecOutputSpy.mockImplementation(() => { + return Promise.resolve({exitCode: 0, stdout: '', stderr: ''}); + }); + maxSatisfyingSpy.mockImplementation(() => inputVersion); + + const dotnetInstaller = new installer.DotnetCoreInstaller( + inputVersion, + inputQuality + ); + + await dotnetInstaller.installDotnet(); + + const scriptArguments = ( + getExecOutputSpy.mock.calls[0][1] as string[] + ).join(' '); + + expect(scriptArguments).toContain( + `-ProxyBypassList ${process.env['no_proxy']}` + ); + }); } - } - ); + }); + + describe('addToPath() tests', () => { + it(`should export DOTNET_ROOT env.var with value from DOTNET_INSTALL_DIR env.var`, async () => { + process.env['DOTNET_INSTALL_DIR'] = 'fictitious/dotnet/install/dir'; + installer.DotnetCoreInstaller.addToPath(); + const dotnet_root = process.env['DOTNET_ROOT']; + expect(dotnet_root).toBe(process.env['DOTNET_INSTALL_DIR']); + }); + + it(`should export value from DOTNET_INSTALL_DIR env.var to the PATH`, async () => { + process.env['DOTNET_INSTALL_DIR'] = 'fictitious/dotnet/install/dir'; + installer.DotnetCoreInstaller.addToPath(); + const path = process.env['PATH']; + expect(path).toContain(process.env['DOTNET_INSTALL_DIR']); + }); + }); + }); + + describe('DotnetVersionResolver tests', () => { + describe('createDotNetVersion() tests', () => { + each([ + '3.1', + '3.x', + '3.1.x', + '3.1.*', + '3.1.X', + '3.1.2', + '3.1.0-preview1' + ]).test( + 'if valid version is supplied (%s), it should return version object with some value', + async version => { + const dotnetVersionResolver = new installer.DotnetVersionResolver( + version + ); + const versionObject = + await dotnetVersionResolver.createDotNetVersion(); + + expect(!!versionObject.value).toBe(true); + } + ); + + each([ + '.', + '..', + ' . ', + '. ', + ' .', + ' . . ', + ' .. ', + ' . ', + '-1.-1', + '-1', + '-1.-1.-1', + '..3', + '1..3', + '1..', + '.2.3', + '.2.x', + '*.', + '1.2.', + '1.2.-abc', + 'a.b', + 'a.b.c', + 'a.b.c-preview', + ' 0 . 1 . 2 ', + 'invalid' + ]).test( + 'if invalid version is supplied (%s), it should throw', + async version => { + const dotnetVersionResolver = new installer.DotnetVersionResolver( + version + ); + + await expect( + async () => await dotnetVersionResolver.createDotNetVersion() + ).rejects.toThrow(); + } + ); + + each(['3', '3.1', '3.1.x', '3.1.*', '3.1.X']).test( + "if version that can be resolved to 'channel' option is supplied (%s), it should set type to 'channel' in version object", + async version => { + const dotnetVersionResolver = new installer.DotnetVersionResolver( + version + ); + const versionObject = + await dotnetVersionResolver.createDotNetVersion(); + + expect(versionObject.type.toLowerCase().includes('channel')).toBe( + true + ); + } + ); + + each(['6.0', '6.0.x', '6.0.*', '6.0.X']).test( + "if version that can be resolved to 'channel' option is supplied and its major tag is >= 6 (%s), it should set type to 'channel' and qualityFlag to 'true' in version object", + async version => { + const dotnetVersionResolver = new installer.DotnetVersionResolver( + version + ); + const versionObject = + await dotnetVersionResolver.createDotNetVersion(); + + expect(versionObject.type.toLowerCase().includes('channel')).toBe( + true + ); + expect(versionObject.qualityFlag).toBe(true); + } + ); + + each(['3.1.2', '3.1.0-preview1']).test( + "if version that can be resolved to 'version' option is supplied (%s), it should set quality flag to 'false' and type to 'version' in version object", + async version => { + const dotnetVersionResolver = new installer.DotnetVersionResolver( + version + ); + const versionObject = + await dotnetVersionResolver.createDotNetVersion(); + + expect(versionObject.type.toLowerCase().includes('version')).toBe( + true + ); + expect(versionObject.qualityFlag).toBe(false); + } + ); + + each(['3.1.2', '3.1']).test( + 'it should create proper line arguments for powershell/bash installation scripts', + async version => { + const dotnetVersionResolver = new installer.DotnetVersionResolver( + version + ); + const versionObject = + await dotnetVersionResolver.createDotNetVersion(); + const windowsRegEx = new RegExp(/^-(Version|Channel)/); + const nonWindowsRegEx = new RegExp(/^--(version|channel)/); + + if (IS_WINDOWS) { + expect(windowsRegEx.test(versionObject.type)).toBe(true); + expect(nonWindowsRegEx.test(versionObject.type)).toBe(false); + } else { + expect(nonWindowsRegEx.test(versionObject.type)).toBe(true); + expect(windowsRegEx.test(versionObject.type)).toBe(false); + } + } + ); + }); + }); }); - -function normalizeFileContents(contents: string): string { - return contents - .trim() - .replace(new RegExp('\r\n', 'g'), '\n') - .replace(new RegExp('\r', 'g'), '\n'); -} - -async function getDotnet(version: string, quality = ''): Promise { - const dotnetInstaller = new installer.DotnetCoreInstaller( - version, - quality as QualityOptions - ); - const installedVersion = await dotnetInstaller.installDotnet(); - installer.DotnetCoreInstaller.addToPath(); - return installedVersion; -} diff --git a/__tests__/setup-dotnet.test.ts b/__tests__/setup-dotnet.test.ts index 76c1da4..831408c 100644 --- a/__tests__/setup-dotnet.test.ts +++ b/__tests__/setup-dotnet.test.ts @@ -1,114 +1,146 @@ -import * as io from '@actions/io'; import * as core from '@actions/core'; import fs from 'fs'; -import os from 'os'; -import path from 'path'; +import semver from 'semver'; +import * as auth from '../src/authutil'; import * as setup from '../src/setup-dotnet'; -import {IS_WINDOWS} from '../src/utils'; -import {IS_LINUX} from '../src/utils'; - -let toolDir: string; - -if (IS_WINDOWS) { - toolDir = path.join(process.env['PROGRAMFILES'] + '', 'dotnet'); -} else if (IS_LINUX) { - toolDir = '/usr/share/dotnet'; -} else { - toolDir = path.join(process.env['HOME'] + '', '.dotnet'); -} - -function createGlobalJsonPath(dotnetVersion: string) { - const globalJsonPath = path.join(process.cwd(), 'global.json'); - const jsonContents = `{${os.EOL}"sdk": {${os.EOL}"version": "${dotnetVersion}"${os.EOL}}${os.EOL}}`; - if (!fs.existsSync(globalJsonPath)) { - fs.writeFileSync(globalJsonPath, jsonContents); - } - return globalJsonPath; -} - -const tempDir = path.join(__dirname, 'runner', 'temp2'); +import {DotnetCoreInstaller} from '../src/installer'; describe('setup-dotnet tests', () => { - const getInputSpy = jest.spyOn(core, 'getInput'); - const getMultilineInputSpy = jest.spyOn(core, 'getMultilineInput'); - const setOutputSpy = jest.spyOn(core, 'setOutput'); - const inputs = {} as any; - beforeAll(async () => { - process.env.RUNNER_TOOL_CACHE = toolDir; - process.env.DOTNET_INSTALL_DIR = toolDir; - process.env.RUNNER_TEMP = tempDir; - try { - await io.rmRF(`${toolDir}/*`); - await io.rmRF(`${tempDir}/*`); - } catch (err) { - console.log(err.message); - console.log('Failed to remove test directories'); - } - }, 30000); + const getInputSpy = jest.spyOn(core, 'getInput'); + const getMultilineInputSpy = jest.spyOn(core, 'getMultilineInput'); + const setFailedSpy = jest.spyOn(core, 'setFailed'); + const debugSpy = jest.spyOn(core, 'debug'); + const infoSpy = jest.spyOn(core, 'info'); + const setOutputSpy = jest.spyOn(core, 'setOutput'); - afterEach(async () => { - try { - await io.rmRF(path.join(process.cwd(), 'global.json')); - await io.rmRF(`${toolDir}/*`); - await io.rmRF(`${tempDir}/*`); - } catch (err) { - console.log(err.message); - console.log('Failed to remove test directories'); - } - }, 30000); + const existsSyncSpy = jest.spyOn(fs, 'existsSync'); - it('Acquires version of dotnet from global.json if no matching version is installed', async () => { - createGlobalJsonPath('3.1.201'); - await setup.run(); + const maxSatisfyingSpy = jest.spyOn(semver, 'maxSatisfying'); - expect(fs.existsSync(path.join(toolDir, 'sdk', '3.1.201'))).toBe(true); - if (IS_WINDOWS) { - expect(fs.existsSync(path.join(toolDir, 'dotnet.exe'))).toBe(true); - } else { - expect(fs.existsSync(path.join(toolDir, 'dotnet'))).toBe(true); - } - }, 400000); + const installDotnetSpy = jest.spyOn( + DotnetCoreInstaller.prototype, + 'installDotnet' + ); + const addToPathSpy = jest.spyOn(DotnetCoreInstaller, 'addToPath'); - it("Sets output with the latest installed by action version if global.json file isn't specified", async () => { - inputs['dotnet-version'] = ['3.1.201', '6.0.401']; + const configAuthenticationSpy = jest.spyOn(auth, 'configAuthentication'); - getMultilineInputSpy.mockImplementation(input => inputs[input]); + describe('run() tests', () => { + beforeEach(() => { + getMultilineInputSpy.mockImplementation(input => inputs[input as string]); + getInputSpy.mockImplementation(input => inputs[input as string]); + }); - await setup.run(); + afterEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + }); - expect(setOutputSpy).toHaveBeenCalledWith('dotnet-version', '6.0.401'); - }, 400000); + it('should fail the action if global-json-file input is present, but the file does not exist in the file system', async () => { + inputs['global-json-file'] = 'fictitious.json'; + inputs['dotnet-version'] = []; - it("Sets output with the version specified in global.json, if it's present", async () => { - createGlobalJsonPath('3.0.103'); + const expectedErrorMessage = `The specified global.json file '${inputs['global-json-file']}' does not exist`; - inputs['dotnet-version'] = ['3.1.201', '6.0.401']; - inputs['global-json-file'] = './global.json'; + await setup.run(); + expect(setFailedSpy).toHaveBeenCalledWith(expectedErrorMessage); + }); - getMultilineInputSpy.mockImplementation(input => inputs[input]); + test(`if 'dotnet-version' and 'global-json-file' inputs aren't present, should log into debug output, try to find global.json in the repo root, fail and log message into info output`, async () => { + inputs['global-json-file'] = ''; + inputs['dotnet-version'] = []; - getInputSpy.mockImplementation(input => inputs[input]); + maxSatisfyingSpy.mockImplementation(() => null); + setOutputSpy.mockImplementation(() => {}); - await setup.run(); + const expectedDebugMessage = + 'No version found, trying to find version from global.json'; + const expectedInfoMessage = `global.json wasn't found in the root directory. No .NET version will be installed.`; - expect(setOutputSpy).toHaveBeenCalledWith('dotnet-version', '3.0.103'); - }, 400000); + await setup.run(); - it('Sets output with the version specified in global.json with absolute path', async () => { - const globalJsonPath = createGlobalJsonPath('3.0.103'); + expect(debugSpy).toHaveBeenCalledWith(expectedDebugMessage); + expect(existsSyncSpy).toHaveBeenCalled(); + expect(infoSpy).toHaveBeenCalledWith(expectedInfoMessage); + }); - inputs['dotnet-version'] = ['3.1.201', '6.0.401']; - inputs['global-json-file'] = globalJsonPath; + it('should fail the action if quality is supplied but its value is not supported', async () => { + inputs['global-json-file'] = ''; + inputs['dotnet-version'] = ['6.0']; + inputs['dotnet-quality'] = 'fictitiousQuality'; - getMultilineInputSpy.mockImplementation(input => inputs[input]); + const expectedErrorMessage = `${inputs['dotnet-quality']} is not a supported value for 'dotnet-quality' option. Supported values are: daily, signed, validated, preview, ga.`; - getInputSpy.mockImplementation(input => inputs[input]); + await setup.run(); + expect(setFailedSpy).toHaveBeenCalledWith(expectedErrorMessage); + }); - await setup.run(); + it('should call installDotnet() multiple times if dotnet-version multiline input is provided', async () => { + inputs['global-json-file'] = ''; + inputs['dotnet-version'] = ['6.0', '7.0']; + inputs['dotnet-quality'] = ''; - expect(setOutputSpy).toHaveBeenCalledWith('dotnet-version', '3.0.103'); - }, 400000); + installDotnetSpy.mockImplementation(() => Promise.resolve('')); + + await setup.run(); + expect(installDotnetSpy).toHaveBeenCalledTimes(2); + }); + + it('should call addToPath() after installation complete', async () => { + inputs['global-json-file'] = ''; + inputs['dotnet-version'] = ['6.0', '7.0']; + inputs['dotnet-quality'] = ''; + + installDotnetSpy.mockImplementation(() => Promise.resolve('')); + addToPathSpy.mockImplementation(() => {}); + + await setup.run(); + expect(addToPathSpy).toHaveBeenCalledTimes(1); + }); + + it('should call auth.configAuthentication() if source-url input is provided', async () => { + inputs['global-json-file'] = ''; + inputs['dotnet-version'] = []; + inputs['dotnet-quality'] = ''; + inputs['source-url'] = 'fictitious.source.url'; + + configAuthenticationSpy.mockImplementation(() => {}); + + await setup.run(); + expect(configAuthenticationSpy).toHaveBeenCalledWith( + inputs['source-url'], + undefined + ); + }); + + it('should call auth.configAuthentication() with proper parameters if source-url and config-file inputs are provided', async () => { + inputs['global-json-file'] = ''; + inputs['dotnet-version'] = []; + inputs['dotnet-quality'] = ''; + inputs['source-url'] = 'fictitious.source.url'; + inputs['config-file'] = 'fictitious.path'; + + configAuthenticationSpy.mockImplementation(() => {}); + setOutputSpy.mockImplementation(() => {}); + + await setup.run(); + expect(configAuthenticationSpy).toHaveBeenCalledWith( + inputs['source-url'], + inputs['config-file'] + ); + }); + + it('should call setOutput() after installation complete', async () => { + inputs['dotnet-version'] = ['6.0.300']; + + installDotnetSpy.mockImplementation(() => Promise.resolve('')); + addToPathSpy.mockImplementation(() => {}); + + await setup.run(); + expect(setOutputSpy).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/externals/install-dotnet.ps1 b/externals/install-dotnet.ps1 index b0b131a..c9a30cb 100644 --- a/externals/install-dotnet.ps1 +++ b/externals/install-dotnet.ps1 @@ -9,6 +9,12 @@ .DESCRIPTION Installs dotnet cli. If dotnet installation already exists in the given directory it will update it only if the requested version differs from the one already installed. + + Note that the intended use of this script is for Continuous Integration (CI) scenarios, where: + - The SDK needs to be installed without user interaction and without admin rights. + - The SDK installation doesn't need to persist across multiple CI runs. + To set up a development environment or to run apps, use installers rather than this script. Visit https://dotnet.microsoft.com/download to get the installer. + .PARAMETER Channel Default: LTS Download from the Channel specified. Possible values: @@ -164,6 +170,12 @@ function Say-Verbose($str) { } } +function Measure-Action($name, $block) { + $time = Measure-Command $block + $totalSeconds = $time.TotalSeconds + Say-Verbose "⏱ Action '$name' took $totalSeconds seconds" +} + function Say-Invocation($Invocation) { $command = $Invocation.MyCommand; $args = (($Invocation.BoundParameters.Keys | foreach { "-$_ `"$($Invocation.BoundParameters[$_])`"" }) -join " ") @@ -1104,10 +1116,10 @@ function Prepare-Install-Directory { } } -Say "Note that the intended use of this script is for Continuous Integration (CI) scenarios, where:" -Say "- The SDK needs to be installed without user interaction and without admin rights." -Say "- The SDK installation doesn't need to persist across multiple CI runs." -Say "To set up a development environment or to run apps, use installers rather than this script. Visit https://dotnet.microsoft.com/download to get the installer.`r`n" +Say-Verbose "Note that the intended use of this script is for Continuous Integration (CI) scenarios, where:" +Say-Verbose "- The SDK needs to be installed without user interaction and without admin rights." +Say-Verbose "- The SDK installation doesn't need to persist across multiple CI runs." +Say-Verbose "To set up a development environment or to run apps, use installers rather than this script. Visit https://dotnet.microsoft.com/download to get the installer.`r`n" if ($SharedRuntime -and (-not $Runtime)) { $Runtime = "dotnet" @@ -1115,14 +1127,16 @@ if ($SharedRuntime -and (-not $Runtime)) { $OverrideNonVersionedFiles = !$SkipNonVersionedFiles -$CLIArchitecture = Get-CLIArchitecture-From-Architecture $Architecture -$NormalizedQuality = Get-NormalizedQuality $Quality -Say-Verbose "Normalized quality: '$NormalizedQuality'" -$NormalizedChannel = Get-NormalizedChannel $Channel -Say-Verbose "Normalized channel: '$NormalizedChannel'" -$NormalizedProduct = Get-NormalizedProduct $Runtime -Say-Verbose "Normalized product: '$NormalizedProduct'" -$FeedCredential = ValidateFeedCredential $FeedCredential +Measure-Action "Product discovery" { + $script:CLIArchitecture = Get-CLIArchitecture-From-Architecture $Architecture + $script:NormalizedQuality = Get-NormalizedQuality $Quality + Say-Verbose "Normalized quality: '$NormalizedQuality'" + $script:NormalizedChannel = Get-NormalizedChannel $Channel + Say-Verbose "Normalized channel: '$NormalizedChannel'" + $script:NormalizedProduct = Get-NormalizedProduct $Runtime + Say-Verbose "Normalized product: '$NormalizedProduct'" + $script:FeedCredential = ValidateFeedCredential $FeedCredential +} $InstallRoot = Resolve-Installation-Path $InstallDir Say-Verbose "InstallRoot: $InstallRoot" @@ -1200,7 +1214,7 @@ if ($DryRun) { return } -Prepare-Install-Directory +Measure-Action "Installation directory preparation" { Prepare-Install-Directory } $ZipPath = [System.IO.Path]::combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName()) Say-Verbose "Zip path: $ZipPath" @@ -1214,7 +1228,7 @@ foreach ($link in $DownloadLinks) Say-Verbose "Downloading `"$($link.type)`" link $($link.downloadLink)" try { - DownloadFile -Source $link.downloadLink -OutPath $ZipPath + Measure-Action "Package download" { DownloadFile -Source $link.downloadLink -OutPath $ZipPath } Say-Verbose "Download succeeded." $DownloadSucceeded = $true $DownloadedLink = $link @@ -1251,7 +1265,7 @@ if (-not $DownloadSucceeded) { } Say "Extracting the archive." -Extract-Dotnet-Package -ZipPath $ZipPath -OutPath $InstallRoot +Measure-Action "Package extraction" { Extract-Dotnet-Package -ZipPath $ZipPath -OutPath $InstallRoot } # Check if the SDK version is installed; if not, fail the installation. $isAssetInstalled = $false @@ -1277,225 +1291,224 @@ if (!$isAssetInstalled) { SafeRemoveFile -Path $ZipPath -Prepend-Sdk-InstallRoot-To-Path -InstallRoot $InstallRoot +Measure-Action "Setting up shell environment" { Prepend-Sdk-InstallRoot-To-Path -InstallRoot $InstallRoot } Say "Note that the script does not resolve dependencies during installation." Say "To check the list of dependencies, go to https://learn.microsoft.com/dotnet/core/install/windows#dependencies" Say "Installed version is $($DownloadedLink.effectiveVersion)" Say "Installation finished" - # SIG # Begin signature block -# MIInzgYJKoZIhvcNAQcCoIInvzCCJ7sCAQExDzANBglghkgBZQMEAgEFADB5Bgor +# MIInvwYJKoZIhvcNAQcCoIInsDCCJ6wCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG -# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCB7pzZ0nuEMd30h -# n1EcAYUQN+1clltqaLf9611TDrw/laCCDYUwggYDMIID66ADAgECAhMzAAACzfNk -# v/jUTF1RAAAAAALNMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD +# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBhfTi3SRn7+vyy +# uCXKPjhiawegWZ493EcaOEycbgkZcKCCDXYwggX0MIID3KADAgECAhMzAAACy7d1 +# OfsCcUI2AAAAAALLMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p -# bmcgUENBIDIwMTEwHhcNMjIwNTEyMjA0NjAyWhcNMjMwNTExMjA0NjAyWjB0MQsw +# bmcgUENBIDIwMTEwHhcNMjIwNTEyMjA0NTU5WhcNMjMwNTExMjA0NTU5WjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB -# AQDrIzsY62MmKrzergm7Ucnu+DuSHdgzRZVCIGi9CalFrhwtiK+3FIDzlOYbs/zz -# HwuLC3hir55wVgHoaC4liQwQ60wVyR17EZPa4BQ28C5ARlxqftdp3H8RrXWbVyvQ -# aUnBQVZM73XDyGV1oUPZGHGWtgdqtBUd60VjnFPICSf8pnFiit6hvSxH5IVWI0iO -# nfqdXYoPWUtVUMmVqW1yBX0NtbQlSHIU6hlPvo9/uqKvkjFUFA2LbC9AWQbJmH+1 -# uM0l4nDSKfCqccvdI5l3zjEk9yUSUmh1IQhDFn+5SL2JmnCF0jZEZ4f5HE7ykDP+ -# oiA3Q+fhKCseg+0aEHi+DRPZAgMBAAGjggGCMIIBfjAfBgNVHSUEGDAWBgorBgEE -# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQU0WymH4CP7s1+yQktEwbcLQuR9Zww -# VAYDVR0RBE0wS6RJMEcxLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJh -# dGlvbnMgTGltaXRlZDEWMBQGA1UEBRMNMjMwMDEyKzQ3MDUzMDAfBgNVHSMEGDAW -# gBRIbmTlUAXTgqoXNzcitW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8v -# d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIw -# MTEtMDctMDguY3JsMGEGCCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDov -# L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDEx -# XzIwMTEtMDctMDguY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIB -# AE7LSuuNObCBWYuttxJAgilXJ92GpyV/fTiyXHZ/9LbzXs/MfKnPwRydlmA2ak0r -# GWLDFh89zAWHFI8t9JLwpd/VRoVE3+WyzTIskdbBnHbf1yjo/+0tpHlnroFJdcDS -# MIsH+T7z3ClY+6WnjSTetpg1Y/pLOLXZpZjYeXQiFwo9G5lzUcSd8YVQNPQAGICl -# 2JRSaCNlzAdIFCF5PNKoXbJtEqDcPZ8oDrM9KdO7TqUE5VqeBe6DggY1sZYnQD+/ -# LWlz5D0wCriNgGQ/TWWexMwwnEqlIwfkIcNFxo0QND/6Ya9DTAUykk2SKGSPt0kL -# tHxNEn2GJvcNtfohVY/b0tuyF05eXE3cdtYZbeGoU1xQixPZAlTdtLmeFNly82uB -# VbybAZ4Ut18F//UrugVQ9UUdK1uYmc+2SdRQQCccKwXGOuYgZ1ULW2u5PyfWxzo4 -# BR++53OB/tZXQpz4OkgBZeqs9YaYLFfKRlQHVtmQghFHzB5v/WFonxDVlvPxy2go -# a0u9Z+ZlIpvooZRvm6OtXxdAjMBcWBAsnBRr/Oj5s356EDdf2l/sLwLFYE61t+ME -# iNYdy0pXL6gN3DxTVf2qjJxXFkFfjjTisndudHsguEMk8mEtnvwo9fOSKT6oRHhM -# 9sZ4HTg/TTMjUljmN3mBYWAWI5ExdC1inuog0xrKmOWVMIIHejCCBWKgAwIBAgIK -# YQ6Q0gAAAAAAAzANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNV +# AQC3sN0WcdGpGXPZIb5iNfFB0xZ8rnJvYnxD6Uf2BHXglpbTEfoe+mO//oLWkRxA +# wppditsSVOD0oglKbtnh9Wp2DARLcxbGaW4YanOWSB1LyLRpHnnQ5POlh2U5trg4 +# 3gQjvlNZlQB3lL+zrPtbNvMA7E0Wkmo+Z6YFnsf7aek+KGzaGboAeFO4uKZjQXY5 +# RmMzE70Bwaz7hvA05jDURdRKH0i/1yK96TDuP7JyRFLOvA3UXNWz00R9w7ppMDcN +# lXtrmbPigv3xE9FfpfmJRtiOZQKd73K72Wujmj6/Su3+DBTpOq7NgdntW2lJfX3X +# a6oe4F9Pk9xRhkwHsk7Ju9E/AgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE +# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUrg/nt/gj+BBLd1jZWYhok7v5/w4w +# RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW +# MBQGA1UEBRMNMjMwMDEyKzQ3MDUyODAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci +# tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j +# b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG +# CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu +# Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0 +# MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAJL5t6pVjIRlQ8j4dAFJ +# ZnMke3rRHeQDOPFxswM47HRvgQa2E1jea2aYiMk1WmdqWnYw1bal4IzRlSVf4czf +# zx2vjOIOiaGllW2ByHkfKApngOzJmAQ8F15xSHPRvNMmvpC3PFLvKMf3y5SyPJxh +# 922TTq0q5epJv1SgZDWlUlHL/Ex1nX8kzBRhHvc6D6F5la+oAO4A3o/ZC05OOgm4 +# EJxZP9MqUi5iid2dw4Jg/HvtDpCcLj1GLIhCDaebKegajCJlMhhxnDXrGFLJfX8j +# 7k7LUvrZDsQniJZ3D66K+3SZTLhvwK7dMGVFuUUJUfDifrlCTjKG9mxsPDllfyck +# 4zGnRZv8Jw9RgE1zAghnU14L0vVUNOzi/4bE7wIsiRyIcCcVoXRneBA3n/frLXvd +# jDsbb2lpGu78+s1zbO5N0bhHWq4j5WMutrspBxEhqG2PSBjC5Ypi+jhtfu3+x76N +# mBvsyKuxx9+Hm/ALnlzKxr4KyMR3/z4IRMzA1QyppNk65Ui+jB14g+w4vole33M1 +# pVqVckrmSebUkmjnCshCiH12IFgHZF7gRwE4YZrJ7QjxZeoZqHaKsQLRMp653beB +# fHfeva9zJPhBSdVcCW7x9q0c2HVPLJHX9YCUU714I+qtLpDGrdbZxD9mikPqL/To +# /1lDZ0ch8FtePhME7houuoPcMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq +# hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x +# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv +# bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 +# IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQG +# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG +# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQg +# Q29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +# CgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03 +# a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akr +# rnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0Rrrg +# OGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy +# 4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9 +# sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAh +# dCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8k +# A/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTB +# w3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmn +# Eyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90 +# lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0w +# ggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2o +# ynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD +# VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBa +# BgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny +# bC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsG +# AQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29t +# L3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNV +# HSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3 +# dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsG +# AQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABl +# AG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKb +# C5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11l +# hJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6 +# I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0 +# wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560 +# STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQam +# ASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGa +# J+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ah +# XJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA +# 9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33Vt +# Y5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr +# /Xmfwb1tbWrJUnMTDXpQzTGCGZ8wghmbAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw +# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN +# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp +# Z25pbmcgUENBIDIwMTECEzMAAALLt3U5+wJxQjYAAAAAAsswDQYJYIZIAWUDBAIB +# BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO +# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIFmuaTXYQ37AFvsEol24fdW+ +# nRqHcc1fr+VQVdqhXc/vMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A +# cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB +# BQAEggEAjY5XW5Ly7TJ1OTbeIR98xU+2dmtw7L71ws+ICnQCGhj2xJDUK+5yrTfO +# 8C98l/P4ynFi33Dl8z2YElqUCuqEXbiCzz06lIL4NuibC5DV/X80ZmICR/NYd2v1 +# ww7IH+7dpsHAowBBindCYpVwQ3Ea3kDWgsjPAinAysFFushSOnNWFvrF6vi2smrs +# smbrAAhEhSfLd1Pxxdw73hQ0YjM/D3F3opaybMQ0blpHhOaqtbiyYzvk0doIzBEc +# trSH4NDIc3yLNj5VbjSczpexE+hyQNY4xCtwco4bVtXhONUihv08AIKR8+sIaI7A +# mM/SWrrwGYSSSxydKqDei7biKG4jDqGCFykwghclBgorBgEEAYI3AwMBMYIXFTCC +# FxEGCSqGSIb3DQEHAqCCFwIwghb+AgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFZBgsq +# hkiG9w0BCRABBKCCAUgEggFEMIIBQAIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl +# AwQCAQUABCB6Hzt2gUb/WZK8fvVnOocriE4rYr6mscZi3gZnBCpiigIGZBr2iMZU +# GBMyMDIzMDMzMTE1MjEwNi41MTZaMASAAgH0oIHYpIHVMIHSMQswCQYDVQQGEwJV +# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE +# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl +# bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNO +# OjA4NDItNEJFNi1DMjlBMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBT +# ZXJ2aWNloIIReDCCBycwggUPoAMCAQICEzMAAAGybkADf26plJIAAQAAAbIwDQYJ +# KoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x +# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv +# bjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwHhcNMjIw +# OTIwMjAyMjAxWhcNMjMxMjE0MjAyMjAxWjCB0jELMAkGA1UEBhMCVVMxEzARBgNV # BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv -# c29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlm -# aWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEw -# OTA5WjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE -# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYD -# VQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG -# 9w0BAQEFAAOCAg8AMIICCgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+la -# UKq4BjgaBEm6f8MMHt03a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc -# 6Whe0t+bU7IKLMOv2akrrnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4D -# dato88tt8zpcoRb0RrrgOGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+ -# lD3v++MrWhAfTVYoonpy4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nk -# kDstrjNYxbc+/jLTswM9sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6 -# A4aN91/w0FK/jJSHvMAhdCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmd -# X4jiJV3TIUs+UsS1Vz8kA/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL -# 5zmhD+kjSbwYuER8ReTBw3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zd -# sGbiwZeBe+3W7UvnSSmnEyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3 -# T8HhhUSJxAlMxdSlQy90lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS -# 4NaIjAsCAwEAAaOCAe0wggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRI -# bmTlUAXTgqoXNzcitW2oynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTAL -# BgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBD -# uRQFTuHqp8cx0SOJNDBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jv -# c29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf -# MDNfMjIuY3JsMF4GCCsGAQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3 -# dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf -# MDNfMjIuY3J0MIGfBgNVHSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEF -# BQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1h -# cnljcHMuaHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkA -# YwB5AF8AcwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn -# 8oalmOBUeRou09h0ZyKbC5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7 -# v0epo/Np22O/IjWll11lhJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0b -# pdS1HXeUOeLpZMlEPXh6I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/ -# KmtYSWMfCWluWpiW5IP0wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvy -# CInWH8MyGOLwxS3OW560STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBp -# mLJZiWhub6e3dMNABQamASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJi -# hsMdYzaXht/a8/jyFqGaJ+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYb -# BL7fQccOKO7eZS/sl/ahXJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbS -# oqKfenoi+kiVH6v7RyOA9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sL -# gOppO6/8MO0ETI7f33VtY5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtX -# cVZOSEXAQsmbdlsKgEhr/Xmfwb1tbWrJUnMTDXpQzTGCGZ8wghmbAgEBMIGVMH4x -# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt -# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01p -# Y3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTECEzMAAALN82S/+NRMXVEAAAAA -# As0wDQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQw -# HAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEINK7 -# cJe0KVfbcXchjID30U/cUg7pWAQUa3+n8JuhjLCLMEIGCisGAQQBgjcCAQwxNDAy -# oBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5j -# b20wDQYJKoZIhvcNAQEBBQAEggEAODLxcflOtjpIXXIhbYyQ0wFeBx0NrmoMU/Ri -# e7CRrAieAbG4iLJzs4DhUov5iuMHY6AAbLWAH54QlSkd4XNp6POsE7lSzN9yjlVw -# V/e0XCaYeXIbtd75hGd5P7wAhM4m2ViDI4IRHyQtjysW0U0F6YiqNlFm7Fzo5Si6 -# l2XxvuEDSdyJcEN70wHQajx6bKfnI/oMY59iGjDXvDP/6cQV9NI0gPHFTwPKA7vg -# PySyVFEG7dQMoEwAWy9GHbcS//RulgUwBhWcrtUP411XLSO6is2VTknwbdglc9HZ -# zViuS4C1ujHlPrlMzm8Y5iGVIQCna5w2NU/kGsSK5+dMkovomKGCFykwghclBgor -# BgEEAYI3AwMBMYIXFTCCFxEGCSqGSIb3DQEHAqCCFwIwghb+AgEDMQ8wDQYJYIZI -# AWUDBAIBBQAwggFZBgsqhkiG9w0BCRABBKCCAUgEggFEMIIBQAIBAQYKKwYBBAGE -# WQoDATAxMA0GCWCGSAFlAwQCAQUABCDRz6ce9oWlH6+o0BtjmAjtvEMN1hfhIA5v -# +wTZHvB4XgIGY2PeyIloGBMyMDIyMTExMDE1MDUxNi43MzRaMASAAgH0oIHYpIHV -# MIHSMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH -# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQL -# EyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsT -# HVRoYWxlcyBUU1MgRVNOOkEyNDAtNEI4Mi0xMzBFMSUwIwYDVQQDExxNaWNyb3Nv -# ZnQgVGltZS1TdGFtcCBTZXJ2aWNloIIReDCCBycwggUPoAMCAQICEzMAAAG4CNTB -# uHngUUkAAQAAAbgwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxEzARBgNV -# BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv -# c29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAg -# UENBIDIwMTAwHhcNMjIwOTIwMjAyMjE2WhcNMjMxMjE0MjAyMjE2WjCB0jELMAkG -# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx -# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9z -# b2Z0IElyZWxhbmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMg -# VFNTIEVTTjpBMjQwLTRCODItMTMwRTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUt -# U3RhbXAgU2VydmljZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJwb -# sfwRHERn5C95QPGn37tJ5vOiY9aWjeIDxpgaXaYGiqsw0G0cvCK3YulrqemEf2Ck -# GSdcOJAF++EqhOSqrO13nGcjqw6hFNnsGwKANyzddwnOO0jz1lfBIIu77TbfNvna -# WbwSRu0DTGHA7n7PR0MYJ9bC/HopStpbFf606LKcTWnwaUuEdAhx6FAqg1rkgugi -# uuaaxKyxRkdjFZLKFXEXL9p01PtwS0fG6vZiRVnEKgeal2TeLvdAIqapBwltPYif -# gqnp7Z4VJMcPo0TWmRNVFOcHRNwWHehN9xg6ugIGXPo7hMpWrPgg4moHO2epc0T3 -# 6rgm9hlDrl28bG5TakmV7NJ98kbF5lgtlrowT6ecwEVtuLd4a0gzYqhanW7zaFZn -# Dft5yMexy59ifETdzpwArj2nJAyIsiq1PY3XPm2mUMLlACksqelHKfWihK/Fehw/ -# mziovBVwkkr/G0F19OWgR+MBUKifwpOyQiLAxrqvVnfCY4QjJCZiHIuS15HCQ/TI -# t/Qj4x1WvRa1UqjnmpLu4/yBYWZsdvZoq8SXI7iOs7muecAJeEkYlM6iOkMighzE -# hjQK9ThPpoAtluXbL7qIHGrfFlHmX/4soc7jj1j8uB31U34gJlB2XphjMaT+E+O9 -# SImk/6GRV9Sm8C88Fnmm2VdwMluCNAUzPFjfvHx3AgMBAAGjggFJMIIBRTAdBgNV -# HQ4EFgQUxP1HJTeFwzNYo1njfucXuUfQaW4wHwYDVR0jBBgwFoAUn6cVXQBeYl2D -# 9OXSZacbUzUZ6XIwXwYDVR0fBFgwVjBUoFKgUIZOaHR0cDovL3d3dy5taWNyb3Nv -# ZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUy -# MDIwMTAoMSkuY3JsMGwGCCsGAQUFBwEBBGAwXjBcBggrBgEFBQcwAoZQaHR0cDov -# L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBUaW1l -# LVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcnQwDAYDVR0TAQH/BAIwADAWBgNVHSUB -# Af8EDDAKBggrBgEFBQcDCDAOBgNVHQ8BAf8EBAMCB4AwDQYJKoZIhvcNAQELBQAD -# ggIBAJ9uk8miwpMoKw3D996piEzbegAGxkABHYn2vP2hbqnkS9U97s/6QlyZOhGF -# sVudaiLeRZZTsaG5hR0oCuBINZ/lelo5xzHc+mBOpBXpxSaW1hqoxaCLsVH1EBtz -# 7in25Hjy+ejuBcilH6EZ0ZtNxmWGIQz8R0AuS0Tj4VgJXHIlXP9dVOiyGo9Velrk -# +FGx/BC+iEuCaKd/IsypHPiCUCh52DGc91s2S7ldQx1H4CljOAtanDfbvSejASWL -# o/s3w0XMAbDurWNns0XidAF2RnL1PaxoOyz9VYakNGK4F3/uJRZnVgbsCYuwNX1B -# mSwM1ZbPSnggNSGTZx/FQ20Jj/ulrK0ryAbvNbNb4kkaS4a767ifCqvUOFLlUT8P -# N43hhldxI6yHPMOWItJpEHIZBiTNKblBsYbIrghb1Ym9tfSsLa5ZJDzVZNndRfhU -# qJOyXF+CVm9OtVmFDG9kIwM6QAX8Q0if721z4VOzZNvD8ktg1lI+XjXgXDJVs3h4 -# 7sMu9GXSYzky+7dtgmc3iRPkda3YVRdmPJtNFN0NLybcssE7vhFCij75eDGQBFq0 -# A4KVG6uBdr6UTWwE0VKHxBz2BpGvn7BCs+5yxnF+HV6CUickDqqPi/II7Zssd9Eb -# P9uzj4luldXDAPrWGtdGq+wK0odlGNVuCMxsL3hn8+KiO9UiMIIHcTCCBVmgAwIB -# AgITMwAAABXF52ueAptJmQAAAAAAFTANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UE -# BhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAc -# BgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0 -# IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwHhcNMjEwOTMwMTgyMjI1 -# WhcNMzAwOTMwMTgzMjI1WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu -# Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv -# cmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCC -# AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAOThpkzntHIhC3miy9ckeb0O -# 1YLT/e6cBwfSqWxOdcjKNVf2AX9sSuDivbk+F2Az/1xPx2b3lVNxWuJ+Slr+uDZn -# hUYjDLWNE893MsAQGOhgfWpSg0S3po5GawcU88V29YZQ3MFEyHFcUTE3oAo4bo3t -# 1w/YJlN8OWECesSq/XJprx2rrPY2vjUmZNqYO7oaezOtgFt+jBAcnVL+tuhiJdxq -# D89d9P6OU8/W7IVWTe/dvI2k45GPsjksUZzpcGkNyjYtcI4xyDUoveO0hyTD4MmP -# frVUj9z6BVWYbWg7mka97aSueik3rMvrg0XnRm7KMtXAhjBcTyziYrLNueKNiOSW -# rAFKu75xqRdbZ2De+JKRHh09/SDPc31BmkZ1zcRfNN0Sidb9pSB9fvzZnkXftnIv -# 231fgLrbqn427DZM9ituqBJR6L8FA6PRc6ZNN3SUHDSCD/AQ8rdHGO2n6Jl8P0zb -# r17C89XYcz1DTsEzOUyOArxCaC4Q6oRRRuLRvWoYWmEBc8pnol7XKHYC4jMYcten -# IPDC+hIK12NvDMk2ZItboKaDIV1fMHSRlJTYuVD5C4lh8zYGNRiER9vcG9H9stQc -# xWv2XFJRXRLbJbqvUAV6bMURHXLvjflSxIUXk8A8FdsaN8cIFRg/eKtFtvUeh17a -# j54WcmnGrnu3tz5q4i6tAgMBAAGjggHdMIIB2TASBgkrBgEEAYI3FQEEBQIDAQAB -# MCMGCSsGAQQBgjcVAgQWBBQqp1L+ZMSavoKRPEY1Kc8Q/y8E7jAdBgNVHQ4EFgQU -# n6cVXQBeYl2D9OXSZacbUzUZ6XIwXAYDVR0gBFUwUzBRBgwrBgEEAYI3TIN9AQEw -# QTA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9E -# b2NzL1JlcG9zaXRvcnkuaHRtMBMGA1UdJQQMMAoGCCsGAQUFBwMIMBkGCSsGAQQB -# gjcUAgQMHgoAUwB1AGIAQwBBMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/ -# MB8GA1UdIwQYMBaAFNX2VsuP6KJcYmjRPZSQW9fOmhjEMFYGA1UdHwRPME0wS6BJ -# oEeGRWh0dHA6Ly9jcmwubWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01p -# Y1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYB -# BQUHMAKGPmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9v -# Q2VyQXV0XzIwMTAtMDYtMjMuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQCdVX38Kq3h -# LB9nATEkW+Geckv8qW/qXBS2Pk5HZHixBpOXPTEztTnXwnE2P9pkbHzQdTltuw8x -# 5MKP+2zRoZQYIu7pZmc6U03dmLq2HnjYNi6cqYJWAAOwBb6J6Gngugnue99qb74p -# y27YP0h1AdkY3m2CDPVtI1TkeFN1JFe53Z/zjj3G82jfZfakVqr3lbYoVSfQJL1A -# oL8ZthISEV09J+BAljis9/kpicO8F7BUhUKz/AyeixmJ5/ALaoHCgRlCGVJ1ijbC -# HcNhcy4sa3tuPywJeBTpkbKpW99Jo3QMvOyRgNI95ko+ZjtPu4b6MhrZlvSP9pEB -# 9s7GdP32THJvEKt1MMU0sHrYUP4KWN1APMdUbZ1jdEgssU5HLcEUBHG/ZPkkvnNt -# yo4JvbMBV0lUZNlz138eW0QBjloZkWsNn6Qo3GcZKCS6OEuabvshVGtqRRFHqfG3 -# rsjoiV5PndLQTHa1V1QJsWkBRH58oWFsc/4Ku+xBZj1p/cvBQUl+fpO+y/g75LcV -# v7TOPqUxUYS8vwLBgqJ7Fx0ViY1w/ue10CgaiQuPNtq6TPmb/wrpNPgkNWcr4A24 -# 5oyZ1uEi6vAnQj0llOZ0dFtq0Z4+7X6gMTN9vMvpe784cETRkPHIqzqKOghif9lw -# Y1NNje6CbaUFEMFxBmoQtB1VM1izoXBm8qGCAtQwggI9AgEBMIIBAKGB2KSB1TCB -# 0jELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl -# ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMk -# TWljcm9zb2Z0IElyZWxhbmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1U -# aGFsZXMgVFNTIEVTTjpBMjQwLTRCODItMTMwRTElMCMGA1UEAxMcTWljcm9zb2Z0 -# IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcGBSsOAwIaAxUAcGteVqFx/IbTKXHL -# euXCPRPMD7uggYMwgYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu -# Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv -# cmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAN -# BgkqhkiG9w0BAQUFAAIFAOcW7qowIhgPMjAyMjExMTAxMTI5NDZaGA8yMDIyMTEx -# MTExMjk0NlowdDA6BgorBgEEAYRZCgQBMSwwKjAKAgUA5xbuqgIBADAHAgEAAgIE -# qTAHAgEAAgIRVjAKAgUA5xhAKgIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEE -# AYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUAA4GB -# AGsU2HTQg158bHX+QngoY7NVfCbGRaLjQOi8geKi26qQWAxll9QLFg4+epiG2nZB -# eQvhxeNmIzounhWfJ+gfhFMi8aBT5z4dLK9iBtmpG1Y14RmSS4andiUlS6bVNVNe -# WGObqHijMVeMOphiTaAfzR6zSASDaG0CfVm9bNBOnZZsMYIEDTCCBAkCAQEwgZMw -# fDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl -# ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMd -# TWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAG4CNTBuHngUUkAAQAA -# AbgwDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRAB -# BDAvBgkqhkiG9w0BCQQxIgQg578XwPrBwneU95xu1sHFncHeCC0UPQ7QK7PvSSby -# VpwwgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9BCAo69Y4oHA7Q4pS+Y1NsBfr -# pIYTeWsPeGTami0X0PD7HzCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQI -# EwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3Nv -# ZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBD -# QSAyMDEwAhMzAAABuAjUwbh54FFJAAEAAAG4MCIEIJVlMK4mQfdJaAZx7Unfka/I -# D4Wbw5edh/SR7TTptzRqMA0GCSqGSIb3DQEBCwUABIICAA1QlR3ywR7e+jqZ++NC -# xsIwREiwVS70CEkbH8XpPRS0mFS0SHcCfpwymGfdep3D0CWk0PIfMhXq0SD97iBI -# rOLdHglVBkMYTjGEBHyBzv/LevAZUuzoc5aqyIF4Ywa5KS4PGbMSuRK5CKAojOzH -# A/vp2/KYuADmf9kOOgOfDVicyfoqZ+3W+QaUI/k0KKV4dPLF55+y18C+Td6sR60Y -# AkcvGZObuj/OkREhdTJ1qJ2E/4RKG8gtGY1DfluLon7+UvS/ciWDWrJnHMmkxM11 -# cYuRIvLArIdq/YS2bcSnY6AVO2zYjj7gCqDN9GuCurstUKC5uxVl3VNxntC0u3Le -# BoI/R5uMYlTXodW8ukLNL6zHrQ4wI4udgW77KJref+3E1PEpZBRMxwose7Vt8lDc -# sW1vdM+eZzUXRLhDR8a0Nai7+PaNoukoGf4pvwsu8Mkeji5a0hWtU9lUVRv6nzue -# 3L2olhsbiHhAET7N6Rj0kzEhbUgfVUJrGvNlWOfN7MDr+OpArGXMPLtovbKTLtXF -# v/GrJo9wQuyqUmY6KQSRDZgOw1CcoZpJcy40HG/aOlJwk03N13OZD5H3KfHwEphR -# YnbGwGq9zUId5druSr5s40Yyl3idAkqmI5SXAm9v/gRq2X9vMU0a7KqXet9wO62F -# TqxV+7Qp48Vw6hW1g+Q7oWoc +# c29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxhbmQgT3Bl +# cmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjowODQyLTRC +# RTYtQzI5QTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZTCC +# AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMqiZTIde/lQ4rC+Bml5f/Wu +# q/xKTxrfbG23HofmQ+qZAN4GyO73PF3y9OAfpt7Qf2jcldWOGUB+HzBuwllYyP3f +# x4MY8zvuAuB37FvoytnNC2DKnVrVlHOVcGUL9CnmhDNMA2/nskjIf2IoiG9J0qLY +# r8duvHdQJ9Li2Pq9guySb9mvUL60ogslCO9gkh6FiEDwMrwUr8Wja6jFpUTny8tg +# 0N0cnCN2w4fKkp5qZcbUYFYicLSb/6A7pHCtX6xnjqwhmJoib3vkKJyVxbuFLRhV +# XxH95b0LHeNhifn3jvo2j+/4QV10jEpXVW+iC9BsTtR69xvTjU51ZgP7BR4YDEWq +# 7JsylSOv5B5THTDXRf184URzFhTyb8OZQKY7mqMh7c8J8w1sEM4XDUF2UZNy829N +# VCzG2tfdEXZaHxF8RmxpQYBxyhZwY1rotuIS+gfN2eq+hkAT3ipGn8/KmDwDtzAb +# nfuXjApgeZqwgcYJ8pDJ+y/xU6ouzJz1Bve5TTihkiA7wQsQe6R60Zk9dPdNzw0M +# K5niRzuQZAt4GI96FhjhlUWcUZOCkv/JXM/OGu/rgSplYwdmPLzzfDtXyuy/GCU5 +# I4l08g6iifXypMgoYkkceOAAz4vx1x0BOnZWfI3fSwqNUvoN7ncTT+MB4Vpvf1QB +# ppjBAQUuvui6eCG0MCVNAgMBAAGjggFJMIIBRTAdBgNVHQ4EFgQUmfIngFzZEZlP +# kjDOVluBSDDaanEwHwYDVR0jBBgwFoAUn6cVXQBeYl2D9OXSZacbUzUZ6XIwXwYD +# VR0fBFgwVjBUoFKgUIZOaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9j +# cmwvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3JsMGwG +# CCsGAQUFBwEBBGAwXjBcBggrBgEFBQcwAoZQaHR0cDovL3d3dy5taWNyb3NvZnQu +# Y29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIw +# MjAxMCgxKS5jcnQwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcD +# CDAOBgNVHQ8BAf8EBAMCB4AwDQYJKoZIhvcNAQELBQADggIBANxHtu3FzIabaDbW +# qswdKBlAhKXRCN+5CSMiv2TYa4i2QuWIm+99piwAhDhADfbqor1zyLi95Y6GQnvI +# WUgdeC7oL1ZtZye92zYK+EIfwYZmhS+CH4infAzUvscHZF3wlrJUfPUIDGVP0lCY +# Vse9mguvG0dqkY4ayQPEHOvJubgZZaOdg/N8dInd6fGeOc+0DoGzB+LieObJ2Q0A +# tEt3XN3iX8Cp6+dZTX8xwE/LvhRwPpb/+nKshO7TVuvenwdTwqB/LT6CNPaElwFe +# KxKrqRTPMbHeg+i+KnBLfwmhEXsMg2s1QX7JIxfvT96md0eiMjiMEO22LbOzmLMN +# d3LINowAnRBAJtX+3/e390B9sMGMHp+a1V+hgs62AopBl0p/00li30DN5wEQ5If3 +# 5Zk7b/T6pEx6rJUDYCti7zCbikjKTanBnOc99zGMlej5X+fC/k5ExUCrOs3/VzGR +# CZt5LvVQSdWqq/QMzTEmim4sbzASK9imEkjNtZZyvC1CsUcD1voFktld4mKMjE+u +# DEV3IddD+DrRk94nVzNPSuZXewfVOnXHSeqG7xM3V7fl2aL4v1OhL2+JwO1Tx3B0 +# irO1O9qbNdJk355bntd1RSVKgM22KFBHnoL7Js7pRhBiaKmVTQGoOb+j1Qa7q+ci +# xGo48Vh9k35BDsJS/DLoXFSPDl4mMIIHcTCCBVmgAwIBAgITMwAAABXF52ueAptJ +# mQAAAAAAFTANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT +# Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m +# dCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNh +# dGUgQXV0aG9yaXR5IDIwMTAwHhcNMjEwOTMwMTgyMjI1WhcNMzAwOTMwMTgzMjI1 +# WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH +# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD +# Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCCAiIwDQYJKoZIhvcNAQEB +# BQADggIPADCCAgoCggIBAOThpkzntHIhC3miy9ckeb0O1YLT/e6cBwfSqWxOdcjK +# NVf2AX9sSuDivbk+F2Az/1xPx2b3lVNxWuJ+Slr+uDZnhUYjDLWNE893MsAQGOhg +# fWpSg0S3po5GawcU88V29YZQ3MFEyHFcUTE3oAo4bo3t1w/YJlN8OWECesSq/XJp +# rx2rrPY2vjUmZNqYO7oaezOtgFt+jBAcnVL+tuhiJdxqD89d9P6OU8/W7IVWTe/d +# vI2k45GPsjksUZzpcGkNyjYtcI4xyDUoveO0hyTD4MmPfrVUj9z6BVWYbWg7mka9 +# 7aSueik3rMvrg0XnRm7KMtXAhjBcTyziYrLNueKNiOSWrAFKu75xqRdbZ2De+JKR +# Hh09/SDPc31BmkZ1zcRfNN0Sidb9pSB9fvzZnkXftnIv231fgLrbqn427DZM9itu +# qBJR6L8FA6PRc6ZNN3SUHDSCD/AQ8rdHGO2n6Jl8P0zbr17C89XYcz1DTsEzOUyO +# ArxCaC4Q6oRRRuLRvWoYWmEBc8pnol7XKHYC4jMYctenIPDC+hIK12NvDMk2ZItb +# oKaDIV1fMHSRlJTYuVD5C4lh8zYGNRiER9vcG9H9stQcxWv2XFJRXRLbJbqvUAV6 +# bMURHXLvjflSxIUXk8A8FdsaN8cIFRg/eKtFtvUeh17aj54WcmnGrnu3tz5q4i6t +# AgMBAAGjggHdMIIB2TASBgkrBgEEAYI3FQEEBQIDAQABMCMGCSsGAQQBgjcVAgQW +# BBQqp1L+ZMSavoKRPEY1Kc8Q/y8E7jAdBgNVHQ4EFgQUn6cVXQBeYl2D9OXSZacb +# UzUZ6XIwXAYDVR0gBFUwUzBRBgwrBgEEAYI3TIN9AQEwQTA/BggrBgEFBQcCARYz +# aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9Eb2NzL1JlcG9zaXRvcnku +# aHRtMBMGA1UdJQQMMAoGCCsGAQUFBwMIMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIA +# QwBBMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNX2 +# VsuP6KJcYmjRPZSQW9fOmhjEMFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwu +# bWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dF8yMDEw +# LTA2LTIzLmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYBBQUHMAKGPmh0dHA6Ly93 +# d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYt +# MjMuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQCdVX38Kq3hLB9nATEkW+Geckv8qW/q +# XBS2Pk5HZHixBpOXPTEztTnXwnE2P9pkbHzQdTltuw8x5MKP+2zRoZQYIu7pZmc6 +# U03dmLq2HnjYNi6cqYJWAAOwBb6J6Gngugnue99qb74py27YP0h1AdkY3m2CDPVt +# I1TkeFN1JFe53Z/zjj3G82jfZfakVqr3lbYoVSfQJL1AoL8ZthISEV09J+BAljis +# 9/kpicO8F7BUhUKz/AyeixmJ5/ALaoHCgRlCGVJ1ijbCHcNhcy4sa3tuPywJeBTp +# kbKpW99Jo3QMvOyRgNI95ko+ZjtPu4b6MhrZlvSP9pEB9s7GdP32THJvEKt1MMU0 +# sHrYUP4KWN1APMdUbZ1jdEgssU5HLcEUBHG/ZPkkvnNtyo4JvbMBV0lUZNlz138e +# W0QBjloZkWsNn6Qo3GcZKCS6OEuabvshVGtqRRFHqfG3rsjoiV5PndLQTHa1V1QJ +# sWkBRH58oWFsc/4Ku+xBZj1p/cvBQUl+fpO+y/g75LcVv7TOPqUxUYS8vwLBgqJ7 +# Fx0ViY1w/ue10CgaiQuPNtq6TPmb/wrpNPgkNWcr4A245oyZ1uEi6vAnQj0llOZ0 +# dFtq0Z4+7X6gMTN9vMvpe784cETRkPHIqzqKOghif9lwY1NNje6CbaUFEMFxBmoQ +# tB1VM1izoXBm8qGCAtQwggI9AgEBMIIBAKGB2KSB1TCB0jELMAkGA1UEBhMCVVMx +# EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT +# FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxh +# bmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjow +# ODQyLTRCRTYtQzI5QTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vy +# dmljZaIjCgEBMAcGBSsOAwIaAxUAjhJ+EeySRfn2KCNsjn9cF9AUSTqggYMwgYCk +# fjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH +# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD +# Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIF +# AOfRUdUwIhgPMjAyMzAzMzEyMDM0MjlaGA8yMDIzMDQwMTIwMzQyOVowdDA6Bgor +# BgEEAYRZCgQBMSwwKjAKAgUA59FR1QIBADAHAgEAAgIKJDAHAgEAAgIRLzAKAgUA +# 59KjVQIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAID +# B6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUAA4GBAJlOESCa/uRR1x6GunE8 +# K/WgHWTpSE31EITDOfTMvDcF4ptngCS5aOc4gfzmhNNehWfP6EOrgoSQzJYZ4YCh +# fYbHNMk56f18sq8t7y2hgR7KixcEo/4HVzeSdaOclHNc4Gn7kCGpMvpT3Xz9Lzc7 +# UKWDZ0zkNKnbS8TZLNueVQwfMYIEDTCCBAkCAQEwgZMwfDELMAkGA1UEBhMCVVMx +# EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT +# FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUt +# U3RhbXAgUENBIDIwMTACEzMAAAGybkADf26plJIAAQAAAbIwDQYJYIZIAWUDBAIB +# BQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQx +# IgQgXhJRuHCXk3arJvifIY3DBe9Ce9EmlP1y6U4XkgL31DkwgfoGCyqGSIb3DQEJ +# EAIvMYHqMIHnMIHkMIG9BCBTeM485+E+t4PEVieUoFKX7PVyLo/nzu+htJPCG04+ +# NTCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw +# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x +# JjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAABsm5A +# A39uqZSSAAEAAAGyMCIEIGGWlnNnYHrB5HguWG0/nJd/WvSrCogze+QCpenu3IM5 +# MA0GCSqGSIb3DQEBCwUABIICADVOLTuNxeEnBOfZpb7Nv4uf91W/Ho5i99zenDSJ +# x5QHVs+bKXmgc3a7/SSsliAT3zygHc7cH4zARbCZePLTivByKmeG08Ka35eyR+FK +# awSNrI/X+eVIC6nw/egCwviBC1NAG8jHGkuScbHeiiGajvS6lp3ORML7UexMuE4w +# 9SEumoghljCLZMwCSvw+3WxhQoBEZroR8u+PID2RdD0vi85FjKPWcZZijVLqHeFi +# TnuFqwRCLTV0MV+dDCbjwXneIqV+AVlnqb9iDMr3ZhISlRcy9XJNpY5vQBj/wqUW +# vefrmpdz0LNkdtXYThPkyl3mha2KsoQi5SA9zSjlAjFgY3ppmXvi3Frbfqk+iL+f +# l/Qc4+B71jG4t28lTWKteJiHqo+6AUXK2rlAl0d74yvhO6N8lMMtXhdJc8JABYn1 +# v2/KKZn5RvPFF8QP7Ac1saIe1+gUFNcsYOLaMm/xl8E6kefWwZnm5Rhm606g1AC/ +# N5Wo08aAs0ymTPH91dEbmOURXLbA3vCyG7kbfgnhCs/j7oQHWaFDzEYuXDIA4ICT +# dxPUTltbq3OWdp0PAS8JSEKPQFaOoQEnPa4adrXWxMvOmel8IGqJiQ+BPOaLQG64 +# Qu2tMkH/5szb1fsEnCe8SJmy5ESF+kmpnLBtJ17Y9o+9nJHF5ddFmvzy+LUaIqDN +# cOfH # SIG # End signature block diff --git a/externals/install-dotnet.sh b/externals/install-dotnet.sh index ecb2b6c..bdaa67e 100755 --- a/externals/install-dotnet.sh +++ b/externals/install-dotnet.sh @@ -1617,6 +1617,10 @@ do echo " $script_name -h|-?|--help" echo "" echo "$script_name is a simple command line interface for obtaining dotnet cli." + echo " Note that the intended use of this script is for Continuous Integration (CI) scenarios, where:" + echo " - The SDK needs to be installed without user interaction and without admin rights." + echo " - The SDK installation doesn't need to persist across multiple CI runs." + echo " To set up a development environment or to run apps, use installers rather than this script. Visit https://dotnet.microsoft.com/download to get the installer." echo "" echo "Options:" echo " -c,--channel Download from the channel specified, Defaults to \`$channel\`." @@ -1694,10 +1698,10 @@ do shift done -say "Note that the intended use of this script is for Continuous Integration (CI) scenarios, where:" -say "- The SDK needs to be installed without user interaction and without admin rights." -say "- The SDK installation doesn't need to persist across multiple CI runs." -say "To set up a development environment or to run apps, use installers rather than this script. Visit https://dotnet.microsoft.com/download to get the installer.\n" +say_verbose "Note that the intended use of this script is for Continuous Integration (CI) scenarios, where:" +say_verbose "- The SDK needs to be installed without user interaction and without admin rights." +say_verbose "- The SDK installation doesn't need to persist across multiple CI runs." +say_verbose "To set up a development environment or to run apps, use installers rather than this script. Visit https://dotnet.microsoft.com/download to get the installer.\n" if [ "$internal" = true ] && [ -z "$(echo $feed_credential)" ]; then message="Provide credentials via --feed-credential parameter." @@ -1731,4 +1735,4 @@ fi say "Note that the script does not resolve dependencies during installation." say "To check the list of dependencies, go to https://learn.microsoft.com/dotnet/core/install, select your operating system and check the \"Dependencies\" section." -say "Installation finished successfully." +say "Installation finished successfully." \ No newline at end of file