* Add fallback install directory when default is not writable * Refactor macOS install directory handling to warn when default is not writable
1795 lines
60 KiB
TypeScript
1795 lines
60 KiB
TypeScript
import {
|
|
afterAll,
|
|
afterEach,
|
|
beforeAll,
|
|
beforeEach,
|
|
describe,
|
|
expect,
|
|
it,
|
|
jest
|
|
} from '@jest/globals';
|
|
import each from 'jest-each';
|
|
import semver from 'semver';
|
|
import fspromises from 'fs/promises';
|
|
import type {Stats} from 'fs';
|
|
import os from 'os';
|
|
import path from 'path';
|
|
|
|
jest.unstable_mockModule('@actions/exec', () => ({
|
|
getExecOutput: jest.fn()
|
|
}));
|
|
jest.unstable_mockModule('@actions/core', () => ({
|
|
warning: jest.fn(),
|
|
info: jest.fn(),
|
|
debug: jest.fn(),
|
|
setOutput: jest.fn(),
|
|
exportVariable: jest.fn((name: string, val: string) => {
|
|
process.env[name] = val;
|
|
}),
|
|
addPath: jest.fn((p: string) => {
|
|
process.env['PATH'] = `${p}${path.delimiter}${process.env['PATH']}`;
|
|
})
|
|
}));
|
|
jest.unstable_mockModule('@actions/io', () => ({
|
|
which: jest.fn()
|
|
}));
|
|
jest.unstable_mockModule('fs', () => {
|
|
const actual = jest.requireActual('fs') as typeof import('fs');
|
|
const chmodSync = jest.fn();
|
|
const lstatSync = jest.fn(actual.lstatSync);
|
|
const mkdtempSync = jest.fn(actual.mkdtempSync);
|
|
const rmSync = jest.fn(actual.rmSync);
|
|
const readdirSync = jest.fn();
|
|
const existsSync = jest.fn(() => true);
|
|
const statSync = jest.fn(() => ({isFile: () => true}));
|
|
const accessSync = jest.fn();
|
|
const mocked = {
|
|
...actual,
|
|
chmodSync,
|
|
lstatSync,
|
|
mkdtempSync,
|
|
rmSync,
|
|
readdirSync,
|
|
existsSync,
|
|
statSync,
|
|
accessSync
|
|
};
|
|
return {
|
|
...mocked,
|
|
default: mocked
|
|
};
|
|
});
|
|
|
|
jest.unstable_mockModule('os', () => {
|
|
const actual = jest.requireActual('os') as typeof import('os');
|
|
// os.homedir() reads the real environment, not jest's process.env.
|
|
const homedir = jest.fn(actual.homedir);
|
|
// Resolved per call, so spies placed on the real module still take effect.
|
|
const arch = () => actual.arch();
|
|
return {...actual, homedir, arch, default: {...actual, homedir, arch}};
|
|
});
|
|
|
|
// Allows tests to re-import the installer as if running on another OS.
|
|
let platformOverride: 'windows' | 'linux' | 'mac' | undefined;
|
|
jest.unstable_mockModule('../src/utils.js', () => {
|
|
const actual = jest.requireActual(
|
|
'../src/utils.js'
|
|
) as typeof import('../src/utils.js');
|
|
const platform = platformOverride ?? actual.PLATFORM;
|
|
return {...actual, PLATFORM: platform, IS_WINDOWS: platform === 'windows'};
|
|
});
|
|
|
|
const exec = await import('@actions/exec');
|
|
const core = await import('@actions/core');
|
|
const io = await import('@actions/io');
|
|
const fs = await import('fs');
|
|
const installer = await import('../src/installer.js');
|
|
const {IS_WINDOWS} = await import('../src/utils.js');
|
|
|
|
describe('installer tests', () => {
|
|
const env = process.env;
|
|
|
|
beforeEach(() => {
|
|
jest.resetModules();
|
|
process.env = {...env};
|
|
(core.exportVariable as jest.Mock).mockImplementation(
|
|
(...args: unknown[]) => {
|
|
const [name, val] = args as [string, string];
|
|
process.env[name] = val;
|
|
}
|
|
);
|
|
(core.addPath as jest.Mock).mockImplementation((...args: unknown[]) => {
|
|
const [p] = args as [string];
|
|
process.env['PATH'] = `${p}${path.delimiter}${process.env['PATH']}`;
|
|
});
|
|
});
|
|
|
|
describe('DotnetCoreInstaller tests', () => {
|
|
const getExecOutputSpy = exec.getExecOutput as jest.Mock;
|
|
const warningSpy = core.warning as jest.Mock;
|
|
const whichSpy = io.which as jest.Mock;
|
|
const maxSatisfyingSpy = jest.spyOn(semver, 'maxSatisfying');
|
|
const chmodSyncSpy = fs.chmodSync as jest.Mock;
|
|
const readdirSpy = jest.spyOn(fspromises, 'readdir');
|
|
|
|
describe('installDotnet() tests', () => {
|
|
beforeAll(() => {
|
|
whichSpy.mockImplementation(() => Promise.resolve('PathToShell'));
|
|
chmodSyncSpy.mockImplementation(() => {});
|
|
readdirSpy.mockImplementation(() => Promise.resolve([]));
|
|
});
|
|
|
|
afterAll(() => {
|
|
jest.resetAllMocks();
|
|
});
|
|
|
|
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 = '10.0.101';
|
|
const inputQuality = '';
|
|
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('should return version of .NET SDK after installation complete', async () => {
|
|
const inputVersion = '10.0.101';
|
|
const inputQuality = '';
|
|
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
|
|
getExecOutputSpy.mockImplementation(() => {
|
|
return Promise.resolve({
|
|
exitCode: 0,
|
|
stdout: `${stdout}`,
|
|
stderr: ''
|
|
});
|
|
});
|
|
maxSatisfyingSpy.mockImplementation(() => inputVersion);
|
|
|
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
|
inputVersion,
|
|
inputQuality
|
|
);
|
|
const installedVersion = await dotnetInstaller.installDotnet();
|
|
|
|
expect(installedVersion).toBe(inputVersion);
|
|
});
|
|
|
|
it(`should supply 'version' argument to the installation script if supplied version is in A.B.C syntax`, async () => {
|
|
const inputVersion = '10.0.101';
|
|
const inputQuality = '';
|
|
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
|
|
|
|
getExecOutputSpy.mockImplementation(() => {
|
|
return Promise.resolve({
|
|
exitCode: 0,
|
|
stdout: `${stdout}`,
|
|
stderr: ''
|
|
});
|
|
});
|
|
maxSatisfyingSpy.mockImplementation(() => inputVersion);
|
|
|
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
|
inputVersion,
|
|
inputQuality
|
|
);
|
|
|
|
await dotnetInstaller.installDotnet();
|
|
|
|
/**
|
|
* First time script would be called to
|
|
* install runtime, here we checking only the
|
|
* second one that installs actual SDK. i.e. 1
|
|
*/
|
|
const callIndex = 1;
|
|
|
|
const scriptArguments = (
|
|
getExecOutputSpy.mock.calls[callIndex][1] as string[]
|
|
).join(' ');
|
|
const expectedArgument = IS_WINDOWS
|
|
? `-Version ${inputVersion}`
|
|
: `--version ${inputVersion}`;
|
|
|
|
expect(scriptArguments).toContain(expectedArgument);
|
|
});
|
|
|
|
it(`should warn if the 'quality' input is set and the supplied version is in A.B.C syntax`, async () => {
|
|
const inputVersion = '10.0.101';
|
|
const inputQuality = 'ga';
|
|
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
|
|
getExecOutputSpy.mockImplementation(() => {
|
|
return Promise.resolve({
|
|
exitCode: 0,
|
|
stdout: `${stdout}`,
|
|
stderr: ''
|
|
});
|
|
});
|
|
maxSatisfyingSpy.mockImplementation(() => inputVersion);
|
|
|
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
|
inputVersion,
|
|
inputQuality
|
|
);
|
|
|
|
await dotnetInstaller.installDotnet();
|
|
|
|
expect(warningSpy).toHaveBeenCalledWith(
|
|
`The 'dotnet-quality' input can be used only with .NET SDK version in A.B, A.B.x, A, A.x and A.B.Cxx formats where the major tag is higher than 5. You specified: ${inputVersion}. 'dotnet-quality' input is ignored.`
|
|
);
|
|
});
|
|
|
|
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';
|
|
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
|
|
|
|
getExecOutputSpy.mockImplementation(() => {
|
|
return Promise.resolve({
|
|
exitCode: 0,
|
|
stdout: `${stdout}`,
|
|
stderr: ''
|
|
});
|
|
});
|
|
maxSatisfyingSpy.mockImplementation(() => inputVersion);
|
|
|
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
|
inputVersion,
|
|
inputQuality
|
|
);
|
|
|
|
await dotnetInstaller.installDotnet();
|
|
|
|
expect(warningSpy).toHaveBeenCalledWith(
|
|
`The 'dotnet-quality' input can be used only with .NET SDK version in A.B, A.B.x, A, A.x and A.B.Cxx formats where the major tag is higher than 5. You specified: ${inputVersion}. 'dotnet-quality' input is ignored.`
|
|
);
|
|
});
|
|
|
|
each(['10', '10.0', '10.0.x', '10.0.*', '10.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';
|
|
const exitCode = 0;
|
|
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
|
|
getExecOutputSpy.mockImplementation(() => {
|
|
return Promise.resolve({
|
|
exitCode: exitCode,
|
|
stdout: `${stdout}`,
|
|
stderr: ''
|
|
});
|
|
});
|
|
maxSatisfyingSpy.mockImplementation(() => inputVersion);
|
|
|
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
|
inputVersion,
|
|
inputQuality
|
|
);
|
|
|
|
await dotnetInstaller.installDotnet();
|
|
|
|
/**
|
|
* First time script would be called to
|
|
* install runtime, here we checking only the
|
|
* second one that installs actual SDK. i.e. 1
|
|
*/
|
|
const callIndex = 1;
|
|
|
|
const scriptArguments = (
|
|
getExecOutputSpy.mock.calls[callIndex][1] as string[]
|
|
).join(' ');
|
|
const expectedArgument = IS_WINDOWS
|
|
? `-Quality ${inputQuality}`
|
|
: `--quality ${inputQuality}`;
|
|
|
|
expect(scriptArguments).toContain(expectedArgument);
|
|
}
|
|
);
|
|
|
|
each(['10', '10.0', '10.0.x', '10.0.*', '10.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 = '';
|
|
const exitCode = 0;
|
|
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
|
|
getExecOutputSpy.mockImplementation(() => {
|
|
return Promise.resolve({
|
|
exitCode: exitCode,
|
|
stdout: `${stdout}`,
|
|
stderr: ''
|
|
});
|
|
});
|
|
maxSatisfyingSpy.mockImplementation(() => inputVersion);
|
|
|
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
|
inputVersion,
|
|
inputQuality
|
|
);
|
|
|
|
await dotnetInstaller.installDotnet();
|
|
|
|
/**
|
|
* First time script would be called to
|
|
* install runtime, here we checking only the
|
|
* second one that installs actual SDK. i.e. 1
|
|
*/
|
|
const callIndex = 1;
|
|
|
|
const scriptArguments = (
|
|
getExecOutputSpy.mock.calls[callIndex][1] as string[]
|
|
).join(' ');
|
|
const expectedArgument = IS_WINDOWS
|
|
? `-Channel 10.0`
|
|
: `--channel 10.0`;
|
|
|
|
expect(scriptArguments).toContain(expectedArgument);
|
|
}
|
|
);
|
|
|
|
if (IS_WINDOWS) {
|
|
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 = '10.0.101';
|
|
const inputQuality = '';
|
|
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
|
|
|
|
getExecOutputSpy.mockImplementation(() => {
|
|
return Promise.resolve({
|
|
exitCode: 0,
|
|
stdout: `${stdout}`,
|
|
stderr: ''
|
|
});
|
|
});
|
|
maxSatisfyingSpy.mockImplementation(() => inputVersion);
|
|
|
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
|
inputVersion,
|
|
inputQuality
|
|
);
|
|
|
|
await dotnetInstaller.installDotnet();
|
|
|
|
/**
|
|
* First time script would be called to
|
|
* install runtime, here we checking only the
|
|
* second one that installs actual SDK. i.e. 1
|
|
*/
|
|
const callIndex = 1;
|
|
|
|
const scriptArguments = (
|
|
getExecOutputSpy.mock.calls[callIndex][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 = '10.0.101';
|
|
const inputQuality = '';
|
|
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
|
|
|
|
getExecOutputSpy.mockImplementation(() => {
|
|
return Promise.resolve({
|
|
exitCode: 0,
|
|
stdout: `${stdout}`,
|
|
stderr: ''
|
|
});
|
|
});
|
|
maxSatisfyingSpy.mockImplementation(() => inputVersion);
|
|
|
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
|
inputVersion,
|
|
inputQuality
|
|
);
|
|
|
|
await dotnetInstaller.installDotnet();
|
|
|
|
/**
|
|
* First time script would be called to
|
|
* install runtime, here we checking only the
|
|
* second one that installs actual SDK. i.e. 1
|
|
*/
|
|
const callIndex = 1;
|
|
|
|
const scriptArguments = (
|
|
getExecOutputSpy.mock.calls[callIndex][1] as string[]
|
|
).join(' ');
|
|
|
|
expect(scriptArguments).toContain(
|
|
`-ProxyBypassList ${process.env['no_proxy']}`
|
|
);
|
|
});
|
|
}
|
|
|
|
it(`should supply 'architecture' argument to the installation script when architecture is provided`, async () => {
|
|
const inputVersion = '10.0.101';
|
|
const inputQuality = '';
|
|
const inputArchitecture = 'x64';
|
|
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
|
|
|
|
getExecOutputSpy.mockImplementation(() => {
|
|
return Promise.resolve({
|
|
exitCode: 0,
|
|
stdout: `${stdout}`,
|
|
stderr: ''
|
|
});
|
|
});
|
|
maxSatisfyingSpy.mockImplementation(() => inputVersion);
|
|
|
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
|
inputVersion,
|
|
inputQuality,
|
|
inputArchitecture
|
|
);
|
|
|
|
await dotnetInstaller.installDotnet();
|
|
|
|
const callIndex = 1;
|
|
const scriptArguments = (
|
|
getExecOutputSpy.mock.calls[callIndex][1] as string[]
|
|
).join(' ');
|
|
const expectedArgument = IS_WINDOWS
|
|
? `-Architecture ${inputArchitecture}`
|
|
: `--architecture ${inputArchitecture}`;
|
|
|
|
expect(scriptArguments).toContain(expectedArgument);
|
|
});
|
|
|
|
it(`should NOT supply 'architecture' argument when architecture is not provided`, async () => {
|
|
const inputVersion = '10.0.101';
|
|
const inputQuality = '';
|
|
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
|
|
|
|
getExecOutputSpy.mockImplementation(() => {
|
|
return Promise.resolve({
|
|
exitCode: 0,
|
|
stdout: `${stdout}`,
|
|
stderr: ''
|
|
});
|
|
});
|
|
maxSatisfyingSpy.mockImplementation(() => inputVersion);
|
|
|
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
|
inputVersion,
|
|
inputQuality
|
|
);
|
|
|
|
await dotnetInstaller.installDotnet();
|
|
|
|
const callIndex = 1;
|
|
const scriptArguments = (
|
|
getExecOutputSpy.mock.calls[callIndex][1] as string[]
|
|
).join(' ');
|
|
|
|
expect(scriptArguments).not.toContain('--architecture');
|
|
expect(scriptArguments).not.toContain('-Architecture');
|
|
});
|
|
|
|
it(`should supply 'install-dir' with arch subdirectory for cross-arch install`, async () => {
|
|
const inputVersion = '10.0.101';
|
|
const inputQuality = '';
|
|
const inputArchitecture = 'x64';
|
|
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
|
|
|
|
getExecOutputSpy.mockImplementation(() => {
|
|
return Promise.resolve({
|
|
exitCode: 0,
|
|
stdout: `${stdout}`,
|
|
stderr: ''
|
|
});
|
|
});
|
|
maxSatisfyingSpy.mockImplementation(() => inputVersion);
|
|
|
|
// Mock os.arch() to return a different arch to simulate cross-arch
|
|
const archSpy = jest.spyOn(os, 'arch').mockReturnValue('arm64');
|
|
|
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
|
inputVersion,
|
|
inputQuality,
|
|
inputArchitecture
|
|
);
|
|
|
|
await dotnetInstaller.installDotnet();
|
|
|
|
const callIndex = 1;
|
|
const scriptArguments = (
|
|
getExecOutputSpy.mock.calls[callIndex][1] as string[]
|
|
).join(' ');
|
|
|
|
const expectedInstallDirFlag = IS_WINDOWS
|
|
? '-InstallDir'
|
|
: '--install-dir';
|
|
|
|
expect(scriptArguments).toContain(expectedInstallDirFlag);
|
|
expect(scriptArguments).toContain(inputArchitecture);
|
|
|
|
archSpy.mockRestore();
|
|
});
|
|
|
|
it(`should NOT supply 'install-dir' when architecture matches runner's native arch`, async () => {
|
|
const inputVersion = '10.0.101';
|
|
const inputQuality = '';
|
|
const nativeArch = os.arch().toLowerCase();
|
|
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
|
|
|
|
getExecOutputSpy.mockImplementation(() => {
|
|
return Promise.resolve({
|
|
exitCode: 0,
|
|
stdout: `${stdout}`,
|
|
stderr: ''
|
|
});
|
|
});
|
|
maxSatisfyingSpy.mockImplementation(() => inputVersion);
|
|
|
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
|
inputVersion,
|
|
inputQuality,
|
|
nativeArch
|
|
);
|
|
|
|
await dotnetInstaller.installDotnet();
|
|
|
|
const callIndex = 1;
|
|
const scriptArguments = (
|
|
getExecOutputSpy.mock.calls[callIndex][1] as string[]
|
|
).join(' ');
|
|
|
|
expect(scriptArguments).not.toContain('--install-dir');
|
|
expect(scriptArguments).not.toContain('-InstallDir');
|
|
});
|
|
});
|
|
|
|
describe('check-latest: false (local SDK reuse) tests', () => {
|
|
const readdirSyncSpy = fs.readdirSync as unknown as jest.Mock;
|
|
const statSyncSpy = fs.statSync as unknown as jest.Mock;
|
|
const accessSyncSpy = fs.accessSync as unknown as jest.Mock;
|
|
|
|
const asFile = {isFile: () => true};
|
|
const muxerName = IS_WINDOWS ? 'dotnet.exe' : 'dotnet';
|
|
const isMuxer = (target: unknown) => String(target).endsWith(muxerName);
|
|
const throwEnoent = () => {
|
|
throw new Error('ENOENT');
|
|
};
|
|
|
|
const makeDirents = (names: string[]): any =>
|
|
names.map(name => ({
|
|
name,
|
|
isDirectory: () => true,
|
|
isSymbolicLink: () => false
|
|
}));
|
|
|
|
const makeSymlinks = (names: string[]): any =>
|
|
names.map(name => ({
|
|
name,
|
|
isDirectory: () => false,
|
|
isSymbolicLink: () => true
|
|
}));
|
|
|
|
beforeEach(() => {
|
|
getExecOutputSpy.mockClear();
|
|
getExecOutputSpy.mockImplementation(() =>
|
|
Promise.resolve({
|
|
exitCode: 0,
|
|
stdout: 'Fictitious dotnet version 1.2.3 is installed',
|
|
stderr: ''
|
|
})
|
|
);
|
|
statSyncSpy.mockReturnValue(asFile);
|
|
accessSyncSpy.mockImplementation(() => undefined);
|
|
});
|
|
|
|
afterEach(() => {
|
|
readdirSyncSpy.mockReset();
|
|
statSyncSpy.mockReset();
|
|
accessSyncSpy.mockReset();
|
|
});
|
|
|
|
interface LocalReuseCase {
|
|
name: string;
|
|
version: string;
|
|
quality?: string;
|
|
dotnetChannel?: string;
|
|
minimumVersion?: string;
|
|
installed: string[];
|
|
expected: string | null;
|
|
}
|
|
|
|
const localReuseCases: LocalReuseCase[] = [
|
|
{
|
|
name: 'reuses a locally installed pinned SDK and skips all install scripts',
|
|
version: '8.0.422',
|
|
installed: ['8.0.100', '8.0.422'],
|
|
expected: '8.0.422'
|
|
},
|
|
{
|
|
name: 'falls back to online install when no local SDK matches the pinned request',
|
|
version: '8.0.422',
|
|
installed: ['8.0.100'],
|
|
expected: null
|
|
},
|
|
{
|
|
name: 'reuses the highest matching patch for a floating A.B.x request',
|
|
version: '8.0.x',
|
|
installed: ['8.0.100', '8.0.412', '8.0.205'],
|
|
expected: '8.0.412'
|
|
},
|
|
{
|
|
name: 'matches the correct feature band for an A.B.Cxx request',
|
|
version: '8.0.1xx',
|
|
installed: ['8.0.100', '8.0.105', '8.0.203'],
|
|
expected: '8.0.105'
|
|
},
|
|
{
|
|
name: 'picks the highest installed SDK for a channel-less latest request',
|
|
version: 'latest',
|
|
installed: ['8.0.412', '9.0.101'],
|
|
expected: '9.0.101'
|
|
},
|
|
{
|
|
name: 'ignores prerelease SDKs when quality is not preview/daily',
|
|
version: '8.0.x',
|
|
installed: ['8.0.100-preview.1', '8.0.100'],
|
|
expected: '8.0.100'
|
|
},
|
|
{
|
|
name: 'reuses the highest installed SDK for a major-only request',
|
|
version: '8',
|
|
installed: ['8.0.100', '8.0.412', '8.0.205'],
|
|
expected: '8.0.412'
|
|
},
|
|
{
|
|
name: 'reuses a prerelease SDK when quality is preview',
|
|
version: '8.0.x',
|
|
quality: 'preview',
|
|
installed: ['8.0.100-preview.1', '8.0.100-preview.2'],
|
|
expected: '8.0.100-preview.2'
|
|
},
|
|
{
|
|
name: 'falls back to online install when no local SDK matches a floating request',
|
|
version: '9.0.x',
|
|
installed: ['8.0.412'],
|
|
expected: null
|
|
},
|
|
{
|
|
name: 'installs online for a latest request with an LTS channel',
|
|
version: 'latest',
|
|
dotnetChannel: 'LTS',
|
|
installed: ['8.0.412', '9.0.101'],
|
|
expected: null
|
|
},
|
|
{
|
|
name: 'reuses the SDK of the requested channel for a latest request',
|
|
version: 'latest',
|
|
dotnetChannel: '8.0',
|
|
installed: ['8.0.412', '9.0.101'],
|
|
expected: '8.0.412'
|
|
},
|
|
{
|
|
name: 'installs online for a wildcard request instead of guessing',
|
|
version: 'x',
|
|
installed: ['8.0.412', '9.0.101'],
|
|
expected: null
|
|
},
|
|
{
|
|
name: 'does not reuse a local SDK below the global.json minimum version',
|
|
version: '8.0',
|
|
minimumVersion: '8.0.400',
|
|
installed: ['8.0.100', '8.0.205'],
|
|
expected: null
|
|
},
|
|
{
|
|
name: 'reuses a local SDK at or above the global.json minimum version',
|
|
version: '8.0',
|
|
minimumVersion: '8.0.400',
|
|
installed: ['8.0.100', '8.0.412'],
|
|
expected: '8.0.412'
|
|
},
|
|
{
|
|
name: 'installs online when quality is preview but only GA SDKs are local',
|
|
version: '8.0.x',
|
|
quality: 'preview',
|
|
installed: ['8.0.412'],
|
|
expected: null
|
|
},
|
|
{
|
|
name: 'ignores the quality input for majors below 6 when matching locally',
|
|
version: '3.1',
|
|
quality: 'preview',
|
|
installed: ['3.1.426', '3.1.500-preview.1'],
|
|
expected: '3.1.426'
|
|
}
|
|
];
|
|
|
|
each(localReuseCases).it(
|
|
'$name',
|
|
async ({
|
|
version,
|
|
quality = '',
|
|
dotnetChannel,
|
|
minimumVersion,
|
|
installed,
|
|
expected
|
|
}: LocalReuseCase) => {
|
|
readdirSyncSpy.mockReturnValue(makeDirents(installed));
|
|
maxSatisfyingSpy.mockImplementation(() => '8.0.412');
|
|
|
|
const installedVersion = await new installer.DotnetCoreInstaller(
|
|
version,
|
|
quality as any,
|
|
undefined,
|
|
dotnetChannel,
|
|
false,
|
|
minimumVersion
|
|
).installDotnet();
|
|
|
|
if (expected === null) {
|
|
expect(getExecOutputSpy).toHaveBeenCalledTimes(2);
|
|
} else {
|
|
expect(installedVersion).toBe(expected);
|
|
expect(getExecOutputSpy).not.toHaveBeenCalled();
|
|
}
|
|
}
|
|
);
|
|
|
|
it('rejects an A.B.Cxx request below .NET 5 instead of reusing a local SDK', async () => {
|
|
readdirSyncSpy.mockReturnValue(makeDirents(['3.1.100']));
|
|
|
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
|
'3.1.1xx',
|
|
'',
|
|
undefined,
|
|
undefined,
|
|
false
|
|
);
|
|
|
|
await expect(dotnetInstaller.installDotnet()).rejects.toThrow(
|
|
`The 'dotnet-version' was supplied in invalid format: 3.1.1xx! The A.B.Cxx syntax is available since the .NET 5.0 release.`
|
|
);
|
|
expect(getExecOutputSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
each(['preview', 'daily'] as const).it(
|
|
"reuses the highest local prerelease SDK for a channel-less latest request with '%s' quality",
|
|
async quality => {
|
|
readdirSyncSpy.mockReturnValue(
|
|
makeDirents(['8.0.412', '9.0.100-preview.1', '9.0.100-preview.2'])
|
|
);
|
|
|
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
|
'latest',
|
|
quality,
|
|
undefined,
|
|
undefined,
|
|
false
|
|
);
|
|
const installedVersion = await dotnetInstaller.installDotnet();
|
|
|
|
expect(installedVersion).toBe('9.0.100-preview.2');
|
|
expect(getExecOutputSpy).not.toHaveBeenCalled();
|
|
}
|
|
);
|
|
|
|
it('does NOT reuse a local SDK for a cross-architecture request', async () => {
|
|
readdirSyncSpy.mockReturnValue(makeDirents(['8.0.412']));
|
|
maxSatisfyingSpy.mockImplementation(() => '8.0.412');
|
|
const archSpy = jest.spyOn(os, 'arch').mockReturnValue('arm64');
|
|
|
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
|
'8.0.x',
|
|
'',
|
|
'x64',
|
|
undefined,
|
|
false
|
|
);
|
|
await dotnetInstaller.installDotnet();
|
|
|
|
expect(getExecOutputSpy).toHaveBeenCalledTimes(2);
|
|
archSpy.mockRestore();
|
|
});
|
|
|
|
it('installs online when check-latest defaults to true even if SDK is local', async () => {
|
|
readdirSyncSpy.mockReturnValue(makeDirents(['8.0.422']));
|
|
maxSatisfyingSpy.mockImplementation(() => '8.0.422');
|
|
|
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
|
'8.0.422',
|
|
''
|
|
);
|
|
await dotnetInstaller.installDotnet();
|
|
|
|
expect(getExecOutputSpy).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('installs online when the dotnet muxer is missing', async () => {
|
|
readdirSyncSpy.mockReturnValue(makeDirents(['8.0.412']));
|
|
statSyncSpy.mockImplementation((target: string) => {
|
|
if (isMuxer(target)) {
|
|
throwEnoent();
|
|
}
|
|
return asFile;
|
|
});
|
|
maxSatisfyingSpy.mockImplementation(() => '8.0.412');
|
|
|
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
|
'8.0.x',
|
|
'',
|
|
undefined,
|
|
undefined,
|
|
false
|
|
);
|
|
await dotnetInstaller.installDotnet();
|
|
|
|
expect(getExecOutputSpy).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
const itOnPosix = IS_WINDOWS ? it.skip : it;
|
|
|
|
itOnPosix(
|
|
'installs online when the dotnet muxer is not executable',
|
|
async () => {
|
|
readdirSyncSpy.mockReturnValue(makeDirents(['8.0.412']));
|
|
accessSyncSpy.mockImplementation((target: string) => {
|
|
if (isMuxer(target)) {
|
|
throw new Error('EACCES');
|
|
}
|
|
});
|
|
maxSatisfyingSpy.mockImplementation(() => '8.0.412');
|
|
|
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
|
'8.0.x',
|
|
'',
|
|
undefined,
|
|
undefined,
|
|
false
|
|
);
|
|
await dotnetInstaller.installDotnet();
|
|
|
|
expect(getExecOutputSpy).toHaveBeenCalledTimes(2);
|
|
}
|
|
);
|
|
|
|
it('installs online when the dotnet muxer path is a directory', async () => {
|
|
readdirSyncSpy.mockReturnValue(makeDirents(['8.0.412']));
|
|
statSyncSpy.mockImplementation((target: string) =>
|
|
isMuxer(target) ? {isFile: () => false} : asFile
|
|
);
|
|
maxSatisfyingSpy.mockImplementation(() => '8.0.412');
|
|
|
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
|
'8.0.x',
|
|
'',
|
|
undefined,
|
|
undefined,
|
|
false
|
|
);
|
|
await dotnetInstaller.installDotnet();
|
|
|
|
expect(getExecOutputSpy).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it('reuses an SDK exposed as a symbolic link', async () => {
|
|
readdirSyncSpy.mockReturnValue(makeSymlinks(['8.0.412']));
|
|
|
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
|
'8.0.x',
|
|
'',
|
|
undefined,
|
|
undefined,
|
|
false
|
|
);
|
|
const installedVersion = await dotnetInstaller.installDotnet();
|
|
|
|
expect(installedVersion).toBe('8.0.412');
|
|
expect(getExecOutputSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('matches the online channel mapping for legacy major-only requests', async () => {
|
|
readdirSyncSpy.mockReturnValue(makeDirents(['3.0.103', '3.1.426']));
|
|
|
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
|
'3',
|
|
'',
|
|
undefined,
|
|
undefined,
|
|
false
|
|
);
|
|
const installedVersion = await dotnetInstaller.installDotnet();
|
|
|
|
expect(installedVersion).toBe('3.1.426');
|
|
expect(getExecOutputSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
each(['8.0.X', '8.0.x', '8.0.*', '8.0']).it(
|
|
'reuses a local SDK for the floating request %s',
|
|
async (version: string) => {
|
|
readdirSyncSpy.mockReturnValue(makeDirents(['8.0.100', '8.0.412']));
|
|
|
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
|
version,
|
|
'',
|
|
undefined,
|
|
undefined,
|
|
false
|
|
);
|
|
const installedVersion = await dotnetInstaller.installDotnet();
|
|
|
|
expect(installedVersion).toBe('8.0.412');
|
|
expect(getExecOutputSpy).not.toHaveBeenCalled();
|
|
}
|
|
);
|
|
|
|
each(['8.X', '8.x', '8.*', '8']).it(
|
|
'reuses a local SDK for the major-only request %s',
|
|
async (version: string) => {
|
|
readdirSyncSpy.mockReturnValue(makeDirents(['8.0.100', '8.0.412']));
|
|
|
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
|
version,
|
|
'',
|
|
undefined,
|
|
undefined,
|
|
false
|
|
);
|
|
const installedVersion = await dotnetInstaller.installDotnet();
|
|
|
|
expect(installedVersion).toBe('8.0.412');
|
|
expect(getExecOutputSpy).not.toHaveBeenCalled();
|
|
}
|
|
);
|
|
|
|
it('does not reuse a local SDK for an uppercase feature band request', async () => {
|
|
readdirSyncSpy.mockReturnValue(makeDirents(['8.0.105']));
|
|
|
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
|
'8.0.1XX',
|
|
'',
|
|
undefined,
|
|
undefined,
|
|
false
|
|
);
|
|
|
|
await expect(dotnetInstaller.installDotnet()).rejects.toThrow(
|
|
`The 'dotnet-version' was supplied in invalid format: 8.0.1XX!`
|
|
);
|
|
});
|
|
|
|
each(['08.0.x', '8.00.x', '08']).it(
|
|
'does not reuse a local SDK for the invalid request %s',
|
|
async (version: string) => {
|
|
readdirSyncSpy.mockReturnValue(makeDirents(['8.0.412']));
|
|
|
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
|
version,
|
|
'',
|
|
undefined,
|
|
undefined,
|
|
false
|
|
);
|
|
|
|
await expect(dotnetInstaller.installDotnet()).rejects.toThrow(
|
|
`The 'dotnet-version' was supplied in invalid format: ${version}!`
|
|
);
|
|
}
|
|
);
|
|
|
|
it('ignores an sdk folder that does not contain an SDK', async () => {
|
|
readdirSyncSpy.mockReturnValue(makeDirents(['8.0.412']));
|
|
statSyncSpy.mockImplementation((target: string) => {
|
|
if (String(target).includes('dotnet.dll')) {
|
|
throwEnoent();
|
|
}
|
|
return asFile;
|
|
});
|
|
maxSatisfyingSpy.mockImplementation(() => '8.0.412');
|
|
|
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
|
'8.0.x',
|
|
'',
|
|
undefined,
|
|
undefined,
|
|
false
|
|
);
|
|
await dotnetInstaller.installDotnet();
|
|
|
|
expect(getExecOutputSpy).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
describe('global.json rollForward tests', () => {
|
|
interface RollForwardCase {
|
|
name: string;
|
|
version?: string;
|
|
quality?: string;
|
|
minimumVersion?: string;
|
|
rollForward?: string;
|
|
installed: string[];
|
|
expected: string | null;
|
|
}
|
|
|
|
const rollForwardCases: RollForwardCase[] = [
|
|
{
|
|
name: 'latestMajor reuses the highest local SDK across majors',
|
|
minimumVersion: '8.0.100',
|
|
rollForward: 'latestMajor',
|
|
installed: ['8.0.412', '9.0.101'],
|
|
expected: '9.0.101'
|
|
},
|
|
{
|
|
name: 'latestMajor ignores local SDKs below the declared version',
|
|
minimumVersion: '8.0.412',
|
|
rollForward: 'latestMajor',
|
|
installed: ['8.0.100', '7.0.400'],
|
|
expected: null
|
|
},
|
|
{
|
|
name: 'latestMajor keeps requiring GA builds when quality is preview',
|
|
quality: 'preview',
|
|
minimumVersion: '8.0.100',
|
|
rollForward: 'latestMajor',
|
|
installed: ['9.0.200-preview.1'],
|
|
expected: null
|
|
},
|
|
{
|
|
name: 'latestMinor reuses a higher local minor of the same major',
|
|
version: '8',
|
|
minimumVersion: '8.1.100',
|
|
rollForward: 'latestMinor',
|
|
installed: ['8.1.200', '8.2.300'],
|
|
expected: '8.2.300'
|
|
},
|
|
{
|
|
name: 'latestMinor does not cross into another major',
|
|
version: '8',
|
|
minimumVersion: '8.1.100',
|
|
rollForward: 'latestMinor',
|
|
installed: ['9.0.101'],
|
|
expected: null
|
|
},
|
|
{
|
|
name: 'latestMinor ignores local SDKs below the declared version',
|
|
version: '8',
|
|
minimumVersion: '8.1.100',
|
|
rollForward: 'latestMinor',
|
|
installed: ['8.0.412'],
|
|
expected: null
|
|
},
|
|
{
|
|
name: 'latestFeature takes the highest band of the declared major.minor',
|
|
version: '8.0',
|
|
minimumVersion: '8.0.100',
|
|
rollForward: 'latestFeature',
|
|
installed: ['8.0.105', '8.0.400'],
|
|
expected: '8.0.400'
|
|
},
|
|
{
|
|
name: 'latestPatch stays inside the declared feature band',
|
|
version: '8.0.2xx',
|
|
minimumVersion: '8.0.200',
|
|
rollForward: 'latestPatch',
|
|
installed: ['8.0.205', '8.0.412'],
|
|
expected: '8.0.205'
|
|
},
|
|
{
|
|
name: 'latestPatch takes the highest patch even when the declared version is installed',
|
|
version: '8.0.1xx',
|
|
minimumVersion: '8.0.100',
|
|
rollForward: 'latestPatch',
|
|
installed: ['8.0.100', '8.0.105'],
|
|
expected: '8.0.105'
|
|
},
|
|
{
|
|
name: 'patch prefers the exact declared version',
|
|
version: '8.0.100',
|
|
minimumVersion: '8.0.100',
|
|
rollForward: 'patch',
|
|
installed: ['8.0.100', '8.0.105'],
|
|
expected: '8.0.100'
|
|
},
|
|
{
|
|
name: 'patch stays inside the declared feature band',
|
|
version: '8.0.100',
|
|
minimumVersion: '8.0.100',
|
|
rollForward: 'patch',
|
|
installed: ['8.0.105', '8.0.200'],
|
|
expected: '8.0.105'
|
|
},
|
|
{
|
|
name: 'feature prefers the declared band over a higher band',
|
|
version: '8.0.100',
|
|
minimumVersion: '8.0.100',
|
|
rollForward: 'feature',
|
|
installed: ['8.0.105', '8.0.400'],
|
|
expected: '8.0.105'
|
|
},
|
|
{
|
|
name: 'feature rolls to the next higher band, not the highest',
|
|
version: '8.0.100',
|
|
minimumVersion: '8.0.100',
|
|
rollForward: 'feature',
|
|
installed: ['8.0.200', '8.0.300', '8.0.400'],
|
|
expected: '8.0.200'
|
|
},
|
|
{
|
|
name: 'feature takes the latest patch of the selected band',
|
|
version: '8.0.100',
|
|
minimumVersion: '8.0.100',
|
|
rollForward: 'feature',
|
|
installed: ['8.0.201', '8.0.205', '8.0.400'],
|
|
expected: '8.0.205'
|
|
},
|
|
{
|
|
name: 'feature does not cross into another minor',
|
|
version: '8.0.100',
|
|
minimumVersion: '8.0.100',
|
|
rollForward: 'feature',
|
|
installed: ['8.1.100'],
|
|
expected: null
|
|
},
|
|
{
|
|
name: 'minor prefers a higher band of the declared minor over a higher minor',
|
|
version: '8.0.100',
|
|
minimumVersion: '8.0.100',
|
|
rollForward: 'minor',
|
|
installed: ['8.0.400', '8.1.100'],
|
|
expected: '8.0.400'
|
|
},
|
|
{
|
|
name: 'minor rolls to the next higher minor, not the highest',
|
|
version: '8.0.100',
|
|
minimumVersion: '8.0.100',
|
|
rollForward: 'minor',
|
|
installed: ['8.1.100', '8.2.100'],
|
|
expected: '8.1.100'
|
|
},
|
|
{
|
|
name: 'major prefers the declared major over a higher major',
|
|
version: '8.0.100',
|
|
minimumVersion: '8.0.100',
|
|
rollForward: 'major',
|
|
installed: ['8.0.400', '9.0.100'],
|
|
expected: '8.0.400'
|
|
},
|
|
{
|
|
name: 'major rolls to the next higher major, not the highest',
|
|
version: '8.0.100',
|
|
minimumVersion: '8.0.100',
|
|
rollForward: 'major',
|
|
installed: ['9.0.100', '10.0.100'],
|
|
expected: '9.0.100'
|
|
},
|
|
{
|
|
name: 'non-latest policies ignore dotnet-quality, as the online path does',
|
|
version: '8.0.100',
|
|
quality: 'preview',
|
|
minimumVersion: '8.0.100',
|
|
rollForward: 'patch',
|
|
installed: ['8.0.105'],
|
|
expected: '8.0.105'
|
|
},
|
|
{
|
|
name: 'non-latest policies never prefer a prerelease over a GA build',
|
|
version: '8.0.100',
|
|
quality: 'preview',
|
|
minimumVersion: '8.0.100',
|
|
rollForward: 'major',
|
|
installed: ['8.0.100', '9.0.100-rc.2'],
|
|
expected: '8.0.100'
|
|
},
|
|
{
|
|
name: 'roll-forward policies never select below the declared version',
|
|
version: '8.0.200',
|
|
minimumVersion: '8.0.200',
|
|
rollForward: 'major',
|
|
installed: ['8.0.100'],
|
|
expected: null
|
|
},
|
|
{
|
|
name: 'disable requires the exact declared version',
|
|
version: '8.0.100',
|
|
installed: ['8.0.200'],
|
|
expected: null
|
|
},
|
|
{
|
|
name: 'installs online for an empty version without a rollForward floor',
|
|
installed: ['8.0.412'],
|
|
expected: null
|
|
}
|
|
];
|
|
|
|
each(rollForwardCases).it(
|
|
'$name',
|
|
async ({
|
|
version = '',
|
|
quality = '',
|
|
minimumVersion,
|
|
rollForward,
|
|
installed,
|
|
expected
|
|
}: RollForwardCase) => {
|
|
readdirSyncSpy.mockReturnValue(makeDirents(installed));
|
|
maxSatisfyingSpy.mockImplementation(() => '8.0.412');
|
|
|
|
const installedVersion = await new installer.DotnetCoreInstaller(
|
|
version,
|
|
quality as any,
|
|
undefined,
|
|
undefined,
|
|
false,
|
|
minimumVersion,
|
|
rollForward
|
|
).installDotnet();
|
|
|
|
if (expected === null) {
|
|
expect(getExecOutputSpy).toHaveBeenCalledTimes(2);
|
|
} else {
|
|
expect(installedVersion).toBe(expected);
|
|
expect(getExecOutputSpy).not.toHaveBeenCalled();
|
|
}
|
|
}
|
|
);
|
|
});
|
|
|
|
describe('skip-lts-prepass tests', () => {
|
|
it('should run the LTS pre-pass by default (skip-lts-prepass not set)', async () => {
|
|
const inputVersion = '9.0.100';
|
|
const inputQuality = '';
|
|
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
|
|
|
|
getExecOutputSpy.mockImplementation(() => {
|
|
return Promise.resolve({exitCode: 0, stdout, stderr: ''});
|
|
});
|
|
maxSatisfyingSpy.mockImplementation(() => inputVersion);
|
|
|
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
|
inputVersion,
|
|
inputQuality
|
|
);
|
|
await dotnetInstaller.installDotnet();
|
|
|
|
expect(getExecOutputSpy).toHaveBeenCalledTimes(2);
|
|
const ltsCallArguments = (
|
|
getExecOutputSpy.mock.calls[0][1] as string[]
|
|
).join(' ');
|
|
expect(ltsCallArguments).toContain(
|
|
IS_WINDOWS ? '-Channel LTS' : '--channel LTS'
|
|
);
|
|
});
|
|
|
|
it('should skip the LTS pre-pass and install only the requested version when skip-lts-prepass is true', async () => {
|
|
const inputVersion = '9.0.100';
|
|
const inputQuality = '';
|
|
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
|
|
|
|
getExecOutputSpy.mockImplementation(() => {
|
|
return Promise.resolve({exitCode: 0, stdout, stderr: ''});
|
|
});
|
|
maxSatisfyingSpy.mockImplementation(() => inputVersion);
|
|
|
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
|
inputVersion,
|
|
inputQuality,
|
|
undefined,
|
|
undefined,
|
|
true,
|
|
undefined,
|
|
undefined,
|
|
true
|
|
);
|
|
const installedVersion = await dotnetInstaller.installDotnet();
|
|
|
|
expect(getExecOutputSpy).toHaveBeenCalledTimes(1);
|
|
const scriptArguments = (
|
|
getExecOutputSpy.mock.calls[0][1] as string[]
|
|
).join(' ');
|
|
expect(scriptArguments).not.toContain('LTS');
|
|
expect(scriptArguments).toContain(
|
|
IS_WINDOWS
|
|
? `-Version ${inputVersion}`
|
|
: `--version ${inputVersion}`
|
|
);
|
|
expect(installedVersion).toBe(inputVersion);
|
|
});
|
|
|
|
it('should skip the LTS pre-pass for a cross-architecture install', async () => {
|
|
const inputVersion = '9.0.100';
|
|
const inputQuality = '';
|
|
const inputArchitecture =
|
|
installer.normalizeArch(os.arch()) === 'x64' ? 'arm64' : 'x64';
|
|
const stdout = `Fictitious dotnet version ${inputVersion} is installed`;
|
|
|
|
getExecOutputSpy.mockImplementation(() => {
|
|
return Promise.resolve({exitCode: 0, stdout, stderr: ''});
|
|
});
|
|
maxSatisfyingSpy.mockImplementation(() => inputVersion);
|
|
|
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
|
inputVersion,
|
|
inputQuality,
|
|
inputArchitecture,
|
|
undefined,
|
|
true,
|
|
undefined,
|
|
undefined,
|
|
true
|
|
);
|
|
await dotnetInstaller.installDotnet();
|
|
|
|
expect(getExecOutputSpy).toHaveBeenCalledTimes(1);
|
|
const scriptArguments = (
|
|
getExecOutputSpy.mock.calls[0][1] as string[]
|
|
).join(' ');
|
|
expect(scriptArguments).not.toContain('LTS');
|
|
expect(scriptArguments).toContain(
|
|
IS_WINDOWS ? '-Architecture' : '--architecture'
|
|
);
|
|
});
|
|
|
|
it('should skip the LTS pre-pass for each requested version', async () => {
|
|
const inputQuality = '';
|
|
|
|
getExecOutputSpy.mockImplementation(() => {
|
|
return Promise.resolve({
|
|
exitCode: 0,
|
|
stdout: `Fictitious dotnet version installed`,
|
|
stderr: ''
|
|
});
|
|
});
|
|
maxSatisfyingSpy.mockImplementation(() => '9.0.100');
|
|
|
|
for (const version of ['8.0.100', '9.0.100']) {
|
|
const dotnetInstaller = new installer.DotnetCoreInstaller(
|
|
version,
|
|
inputQuality,
|
|
undefined,
|
|
undefined,
|
|
true,
|
|
undefined,
|
|
undefined,
|
|
true
|
|
);
|
|
await dotnetInstaller.installDotnet();
|
|
}
|
|
|
|
// 2 versions x 1 call each (no LTS pre-pass call for either)
|
|
expect(getExecOutputSpy).toHaveBeenCalledTimes(2);
|
|
for (const call of getExecOutputSpy.mock.calls) {
|
|
expect((call[1] as string[]).join(' ')).not.toContain('LTS');
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
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.DotnetInstallDir.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.DotnetInstallDir.addToPath();
|
|
const path = process.env['PATH'];
|
|
expect(path).toContain(process.env['DOTNET_INSTALL_DIR']);
|
|
});
|
|
});
|
|
|
|
describe('install directory fallback tests', () => {
|
|
// Mock factories re-run on resetModules, so the installer sees new
|
|
// instances; configure those, not the ones captured at file load.
|
|
const importInstallerFor = async (
|
|
platform: 'windows' | 'linux' | 'mac',
|
|
isWritable: (dir: string) => boolean
|
|
) => {
|
|
platformOverride = platform;
|
|
jest.resetModules();
|
|
|
|
const freshFs = await import('fs');
|
|
(freshFs.lstatSync as jest.Mock).mockReturnValue({} as Stats);
|
|
(freshFs.mkdtempSync as jest.Mock).mockImplementation(
|
|
(...args: unknown[]) => {
|
|
const prefix = String(args[0]);
|
|
if (!isWritable(path.dirname(prefix))) {
|
|
throw Object.assign(new Error('permission denied'), {
|
|
code: 'EACCES'
|
|
});
|
|
}
|
|
return `${prefix}abc123`;
|
|
}
|
|
);
|
|
(freshFs.rmSync as jest.Mock).mockImplementation(() => {});
|
|
|
|
const freshCore = await import('@actions/core');
|
|
const freshOs = await import('os');
|
|
const {DotnetInstallDir} = await import('../src/installer.js');
|
|
|
|
return {DotnetInstallDir, core: freshCore, fs: freshFs, os: freshOs};
|
|
};
|
|
|
|
const probedDirs = (freshFs: typeof import('fs')) =>
|
|
(freshFs.mkdtempSync as jest.Mock).mock.calls.map(call =>
|
|
path.dirname(String(call[0]))
|
|
);
|
|
|
|
beforeEach(() => {
|
|
delete process.env['DOTNET_INSTALL_DIR'];
|
|
});
|
|
|
|
afterEach(() => {
|
|
platformOverride = undefined;
|
|
});
|
|
|
|
it(`should use the default location when it is writable`, async () => {
|
|
const {DotnetInstallDir} = await importInstallerFor(
|
|
'linux',
|
|
() => true
|
|
);
|
|
|
|
expect(DotnetInstallDir.dirPath).toBe('/usr/share/dotnet');
|
|
});
|
|
|
|
it(`should fall back to the home directory when the default location is not writable`, async () => {
|
|
const fallbackPath = path.join(os.homedir(), '.dotnet');
|
|
const {DotnetInstallDir, core: freshCore} = await importInstallerFor(
|
|
'linux',
|
|
dir => dir === fallbackPath
|
|
);
|
|
|
|
expect(DotnetInstallDir.dirPath).toBe(fallbackPath);
|
|
expect(freshCore.warning).toHaveBeenCalled();
|
|
});
|
|
|
|
it(`should reject a dangling symlink instead of probing its writable parent`, async () => {
|
|
// Resolved, because that is the form the probe receives on every platform.
|
|
const linkPath = path.resolve('/usr/share/dotnet');
|
|
const {DotnetInstallDir, fs: freshFs} = await importInstallerFor(
|
|
'linux',
|
|
() => true
|
|
);
|
|
// A dangling link exists for lstat, but nothing can be created under it.
|
|
(freshFs.mkdtempSync as jest.Mock).mockImplementation(
|
|
(...args: unknown[]) => {
|
|
if (path.dirname(String(args[0])) === linkPath) {
|
|
throw Object.assign(new Error('no such file or directory'), {
|
|
code: 'ENOENT'
|
|
});
|
|
}
|
|
return `${args[0]}abc123`;
|
|
}
|
|
);
|
|
|
|
expect(DotnetInstallDir.dirPath).toBe(
|
|
path.join(os.homedir(), '.dotnet')
|
|
);
|
|
const probed = probedDirs(freshFs);
|
|
expect(probed).toContain(linkPath);
|
|
expect(probed).not.toContain(path.dirname(linkPath));
|
|
});
|
|
|
|
it(`should probe the nearest existing parent when the directory is missing`, async () => {
|
|
const systemPath = path.resolve('/usr/share/dotnet');
|
|
const parentPath = path.dirname(systemPath);
|
|
const {DotnetInstallDir, fs: freshFs} = await importInstallerFor(
|
|
'linux',
|
|
() => true
|
|
);
|
|
// Only the parent exists, so the walk has to climb before probing.
|
|
(freshFs.lstatSync as jest.Mock).mockImplementation(
|
|
(...args: unknown[]) =>
|
|
path.resolve(String(args[0])) === parentPath
|
|
? ({} as Stats)
|
|
: undefined
|
|
);
|
|
|
|
expect(DotnetInstallDir.dirPath).toBe('/usr/share/dotnet');
|
|
const probed = probedDirs(freshFs);
|
|
expect(probed).toContain(parentPath);
|
|
expect(probed).not.toContain(systemPath);
|
|
});
|
|
|
|
it(`should give up at the filesystem root when no ancestor exists`, async () => {
|
|
const systemPath = path.resolve('/usr/share/dotnet');
|
|
const homePath = path.join(os.homedir(), '.dotnet');
|
|
const {DotnetInstallDir, fs: freshFs} = await importInstallerFor(
|
|
'linux',
|
|
() => true
|
|
);
|
|
// Nothing on the system path exists, all the way up to the root.
|
|
(freshFs.lstatSync as jest.Mock).mockImplementation(
|
|
(...args: unknown[]) =>
|
|
systemPath.startsWith(path.resolve(String(args[0])))
|
|
? undefined
|
|
: ({} as Stats)
|
|
);
|
|
|
|
expect(DotnetInstallDir.dirPath).toBe(homePath);
|
|
// The walk hit the root and returned without ever probing.
|
|
expect(probedDirs(freshFs)).toEqual([homePath]);
|
|
});
|
|
|
|
it(`should not probe a relative default location`, async () => {
|
|
// An unset HOME makes the macOS default relative, e.g. 'undefined/.dotnet'.
|
|
delete process.env['HOME'];
|
|
const homePath = path.join(os.homedir(), '.dotnet');
|
|
const {DotnetInstallDir, fs: freshFs} = await importInstallerFor(
|
|
'mac',
|
|
() => true
|
|
);
|
|
|
|
expect(DotnetInstallDir.dirPath).toBe(homePath);
|
|
expect(probedDirs(freshFs)).not.toContain(process.cwd());
|
|
});
|
|
|
|
it(`should not probe a relative default location on Windows`, async () => {
|
|
// An unset PROGRAMFILES makes the Windows default relative.
|
|
delete process.env['PROGRAMFILES'];
|
|
const homePath = path.join(os.homedir(), '.dotnet');
|
|
const {DotnetInstallDir, fs: freshFs} = await importInstallerFor(
|
|
'windows',
|
|
() => true
|
|
);
|
|
|
|
expect(DotnetInstallDir.dirPath).toBe(homePath);
|
|
expect(probedDirs(freshFs)).not.toContain(process.cwd());
|
|
});
|
|
|
|
it(`should not treat the filesystem root as a home directory`, async () => {
|
|
const root = path.parse(process.cwd()).root;
|
|
const rootDotnet = path.join(root, '.dotnet');
|
|
// Only the root candidate is writable, so an unguarded run would pick it.
|
|
const {
|
|
DotnetInstallDir,
|
|
os: freshOs,
|
|
core: freshCore
|
|
} = await importInstallerFor('linux', dir => dir === rootDotnet);
|
|
(freshOs.homedir as jest.Mock).mockReturnValue(root);
|
|
|
|
expect(DotnetInstallDir.dirPath).toBe('/usr/share/dotnet');
|
|
expect(freshCore.warning).toHaveBeenCalled();
|
|
});
|
|
|
|
it(`should still report writable when the probe cannot be removed`, async () => {
|
|
const {DotnetInstallDir, fs: freshFs} = await importInstallerFor(
|
|
'linux',
|
|
() => true
|
|
);
|
|
(freshFs.rmSync as jest.Mock).mockImplementation(() => {
|
|
throw Object.assign(new Error('device or resource busy'), {
|
|
code: 'EBUSY'
|
|
});
|
|
});
|
|
|
|
expect(DotnetInstallDir.dirPath).toBe('/usr/share/dotnet');
|
|
});
|
|
|
|
it(`should keep the shared macOS location and warn when it is not writable`, async () => {
|
|
// The mac default derives from HOME, homeInstallPath() from os.homedir();
|
|
// aligning them is what makes both candidates the same path.
|
|
process.env['HOME'] = os.homedir();
|
|
const homePath = path.join(os.homedir(), '.dotnet');
|
|
const {
|
|
DotnetInstallDir,
|
|
fs: freshFs,
|
|
core: freshCore
|
|
} = await importInstallerFor('mac', () => false);
|
|
|
|
expect(DotnetInstallDir.dirPath).toBe(homePath);
|
|
// One candidate, so it is probed once rather than twice.
|
|
expect(probedDirs(freshFs)).toEqual([homePath]);
|
|
expect(freshCore.warning).toHaveBeenCalled();
|
|
});
|
|
|
|
it(`should prefer DOTNET_INSTALL_DIR env.var without probing`, async () => {
|
|
process.env['DOTNET_INSTALL_DIR'] = path.join(path.sep, 'custom');
|
|
const {DotnetInstallDir, fs: freshFs} = await importInstallerFor(
|
|
'linux',
|
|
() => false
|
|
);
|
|
|
|
expect(DotnetInstallDir.dirPath).toBe(path.join(path.sep, 'custom'));
|
|
expect(freshFs.mkdtempSync as jest.Mock).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it(`should resolve once, and not before the directory is needed`, async () => {
|
|
const {DotnetInstallDir, fs: freshFs} = await importInstallerFor(
|
|
'linux',
|
|
() => true
|
|
);
|
|
const probe = freshFs.mkdtempSync as jest.Mock;
|
|
|
|
expect(probe).not.toHaveBeenCalled();
|
|
|
|
void DotnetInstallDir.dirPath;
|
|
const afterFirstRead = probe.mock.calls.length;
|
|
void DotnetInstallDir.dirPath;
|
|
|
|
expect(afterFirstRead).toBeGreaterThan(0);
|
|
expect(probe.mock.calls.length).toBe(afterFirstRead);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('normalizeArch() tests', () => {
|
|
it(`should normalize 'amd64' to 'x64'`, () => {
|
|
expect(installer.normalizeArch('amd64')).toBe('x64');
|
|
});
|
|
|
|
it(`should normalize 'AMD64' to 'x64' (case-insensitive)`, () => {
|
|
expect(installer.normalizeArch('AMD64')).toBe('x64');
|
|
});
|
|
|
|
it(`should pass through 'x64' unchanged`, () => {
|
|
expect(installer.normalizeArch('x64')).toBe('x64');
|
|
});
|
|
|
|
it(`should pass through 'arm64' unchanged`, () => {
|
|
expect(installer.normalizeArch('arm64')).toBe('arm64');
|
|
});
|
|
|
|
it(`should lowercase 'ARM64'`, () => {
|
|
expect(installer.normalizeArch('ARM64')).toBe('arm64');
|
|
});
|
|
|
|
it(`should pass through 'x86' unchanged`, () => {
|
|
expect(installer.normalizeArch('x86')).toBe('x86');
|
|
});
|
|
});
|
|
|
|
describe('DotnetVersionResolver tests', () => {
|
|
describe('createDotnetVersion() tests', () => {
|
|
each([
|
|
'10.0',
|
|
'10.x',
|
|
'10.0.x',
|
|
'10.0.*',
|
|
'10.0.X',
|
|
'10.0.0',
|
|
'10.0.0-preview7',
|
|
'10.0.1xx'
|
|
]).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(['10', '10.0', '10.0.x', '10.0.*', '10.0.X', '10.0.1xx']).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(['10.0', '10.0.x', '10.0.*', '10.0.X', '10.0.1xx']).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(['10.0.0', '10.0.0-preview7']).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(['10.0.0', '10.0']).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);
|
|
}
|
|
}
|
|
);
|
|
|
|
it(`should throw if dotnet-version is supplied in A.B.Cxx syntax with major tag lower that 5`, async () => {
|
|
const version = '3.0.1xx';
|
|
const dotnetVersionResolver = new installer.DotnetVersionResolver(
|
|
version
|
|
);
|
|
await expect(
|
|
async () => await dotnetVersionResolver.createDotnetVersion()
|
|
).rejects.toThrow(
|
|
`'dotnet-version' was supplied in invalid format: ${version}! The A.B.Cxx syntax is available since the .NET 5.0 release.`
|
|
);
|
|
});
|
|
});
|
|
});
|
|
});
|