Improve desktop script runs with timestamps & logs

Improve script execution in the desktop app by introducing timestamped
filenames and detailed logging. These changes aim to facilitate easier
debugging, auditing and overall better user experience.

Key changes:

- Add timestamps in filenames for temporary files to aid in
  troubleshooting and auditing.
- Add application logging throughout the script execution process to
  enhance troubleshooting capabilities.

Other supporting changes:

- Refactor `TemporaryFileCodeRunner` with subfunctions for improved
  readability, maintenance, reusability and extensibility.
- Refactor unit tests for `TemporaryFileCodeRunner` for improved
  granularity and simplicity.
- Create centralized definition of supported operating systems by
  privacy.sexy to ensure robust and consistent test case creation.
- Simplify the `runCode` method by removing the file extension
  parameter; now handled internally by `FileNameGenerator`.
This commit is contained in:
undergroundwires
2023-12-31 14:28:58 +01:00
parent 8f4b34f8f1
commit cdc32d1f12
11 changed files with 465 additions and 202 deletions

View File

@@ -2,6 +2,8 @@ import { OperatingSystem } from '@/domain/OperatingSystem';
export interface CodeRunner {
runCode(
code: string, folderName: string, fileExtension: string, os: OperatingSystem,
code: string,
tempScriptFolderName: string,
os: OperatingSystem,
): Promise<void>;
}

View File

@@ -0,0 +1,35 @@
import { OperatingSystem } from '@/domain/OperatingSystem';
/**
* Generates a timestamped filename specific to the given operating system.
*
* The filename includes:
* - A timestamp for uniqueness and easier auditability.
* - File extension based on the operating system.
*/
export function generateOsTimestampedFileName(
currentOs: OperatingSystem,
date = new Date(),
): string {
const baseFileName = `run-${createTimeStampForFile(date)}`;
const extension = FileExtensions[currentOs];
return extension ? `${baseFileName}.${extension}` : baseFileName;
}
const FileExtensions: Partial<Record<OperatingSystem, string>> = {
// '.bat' for Windows batch files; required for executability.
[OperatingSystem.Windows]: 'bat',
// '.sh' for Unix-like systems; enhances recognition as a shell script
[OperatingSystem.macOS]: 'sh',
[OperatingSystem.Linux]: 'sh',
};
/** Generates a timestamp for the filename in 'YYYY-MM-DD_HH-MM-SS' format. */
function createTimeStampForFile(date: Date): string {
return date
.toISOString()
.replace(/T/, '_')
.replace(/:/g, '-')
.replace(/\..+/, '');
}

View File

@@ -1,37 +1,88 @@
import { OperatingSystem } from '@/domain/OperatingSystem';
import { CodeRunner } from '@/application/CodeRunner';
import { Logger } from '@/application/Common/Log/Logger';
import { ElectronLogger } from '../Log/ElectronLogger';
import { SystemOperations } from './SystemOperations/SystemOperations';
import { createNodeSystemOperations } from './SystemOperations/NodeSystemOperations';
import { generateOsTimestampedFileName } from './FileNameGenerator';
export type FileNameGenerator = (os: OperatingSystem) => string;
export class TemporaryFileCodeRunner implements CodeRunner {
constructor(
private readonly system: SystemOperations = createNodeSystemOperations(),
private readonly fileNameGenerator: FileNameGenerator = generateOsTimestampedFileName,
private readonly logger: Logger = ElectronLogger,
) { }
public async runCode(
code: string,
folderName: string,
fileExtension: string,
tempScriptFolderName: string,
os: OperatingSystem,
): Promise<void> {
const dir = this.system.location.combinePaths(
this.logger.info(`Starting running code for OS: ${OperatingSystem[os]}`);
try {
const fileName = this.fileNameGenerator(os);
const filePath = await this.createTemporaryFile(fileName, tempScriptFolderName, code);
await this.executeFile(filePath, os);
this.logger.info(`Successfully executed script at ${filePath}`);
} catch (error) {
this.logger.error(`Error executing script: ${error.message}`, error);
throw error;
}
}
private async createTemporaryFile(
fileName: string,
tempScriptFolderName: string,
contents: string,
): Promise<string> {
const directoryPath = this.system.location.combinePaths(
this.system.operatingSystem.getTempDirectory(),
folderName,
tempScriptFolderName,
);
await this.system.fileSystem.createDirectory(dir, true);
const filePath = this.system.location.combinePaths(dir, `run.${fileExtension}`);
await this.system.fileSystem.writeToFile(filePath, code);
await this.system.fileSystem.setFilePermissions(filePath, '755');
await this.createDirectoryIfNotExists(directoryPath);
const filePath = this.system.location.combinePaths(directoryPath, fileName);
await this.createFile(filePath, contents);
return filePath;
}
private async createFile(filePath: string, contents: string): Promise<void> {
this.logger.info(`Creating file at ${filePath}, size: ${contents.length} characters`);
await this.system.fileSystem.writeToFile(filePath, contents);
this.logger.info(`File created successfully at ${filePath}`);
}
private async createDirectoryIfNotExists(directoryPath: string): Promise<void> {
this.logger.info(`Checking and ensuring directory exists: ${directoryPath}`);
await this.system.fileSystem.createDirectory(directoryPath, true);
this.logger.info(`Directory confirmed at: ${directoryPath}`);
}
private async executeFile(filePath: string, os: OperatingSystem): Promise<void> {
await this.setFileExecutablePermissions(filePath);
const command = getExecuteCommand(filePath, os);
await this.executeCommand(command);
}
private async setFileExecutablePermissions(filePath: string): Promise<void> {
this.logger.info(`Setting execution permissions for file at ${filePath}`);
await this.system.fileSystem.setFilePermissions(filePath, '755');
this.logger.info(`Execution permissions set successfully for ${filePath}`);
}
private async executeCommand(command: string): Promise<void> {
this.logger.info(`Executing command: ${command}`);
await this.system.command.execute(command);
this.logger.info('Command executed successfully.');
}
}
function getExecuteCommand(
scriptPath: string,
currentOperatingSystem: OperatingSystem,
os: OperatingSystem,
): string {
switch (currentOperatingSystem) {
switch (os) {
case OperatingSystem.Linux:
return `x-terminal-emulator -e '${scriptPath}'`;
case OperatingSystem.macOS:
@@ -42,6 +93,6 @@ function getExecuteCommand(
case OperatingSystem.Windows:
return scriptPath;
default:
throw Error(`unsupported os: ${OperatingSystem[currentOperatingSystem]}`);
throw Error(`unsupported os: ${OperatingSystem[os]}`);
}
}

View File

@@ -30,7 +30,6 @@ export default defineComponent({
await codeRunner.runCode(
currentContext.state.code.current,
currentContext.app.info.name,
currentState.value.collection.scripting.fileExtension,
os,
);
}

View File

@@ -2,6 +2,7 @@ import { ViewType } from '@/presentation/components/Scripts/Menu/View/ViewType';
import { getEnumValues } from '@/application/Common/Enum';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { getOperatingSystemDisplayName } from '@/presentation/components/Shared/OperatingSystemNames';
import { AllSupportedOperatingSystems } from '@tests/shared/TestCases/SupportedOperatingSystems';
describe('operating system selector', () => {
// Regression test for a bug where switching between operating systems caused uncaught exceptions.
@@ -29,11 +30,7 @@ function getSupportedOperatingSystemsList() {
the application within Cypress, as its compilation is tightly coupled with Vite.
Ideally, this should dynamically fetch the list from the compiled application.
*/
return [
OperatingSystem.Windows,
OperatingSystem.Linux,
OperatingSystem.macOS,
];
return AllSupportedOperatingSystems;
}
function assertExpectedActions() {

View File

@@ -0,0 +1,11 @@
import { OperatingSystem } from '@/domain/OperatingSystem';
export type SupportedOperatingSystem = OperatingSystem.Windows
| OperatingSystem.Linux
| OperatingSystem.macOS;
export const AllSupportedOperatingSystems: readonly OperatingSystem[] = [
OperatingSystem.Windows,
OperatingSystem.Linux,
OperatingSystem.macOS,
] as const;

View File

@@ -5,6 +5,7 @@ import { EnumRangeTestRunner } from '@tests/unit/application/Common/EnumRangeTes
import { VersionStub } from '@tests/unit/shared/Stubs/VersionStub';
import { Version } from '@/domain/Version';
import { PropertyKeys } from '@/TypeHelpers';
import { SupportedOperatingSystem, AllSupportedOperatingSystems } from '@tests/shared/TestCases/SupportedOperatingSystems';
describe('ProjectInformation', () => {
describe('retrieval of property values', () => {
@@ -117,48 +118,42 @@ describe('ProjectInformation', () => {
});
});
});
describe('correct retrieval of download URL per operating system', () => {
const testCases: ReadonlyArray<{
readonly os: OperatingSystem,
describe('correct retrieval of download URL for every supported operating system', () => {
const testCases: Record<SupportedOperatingSystem, {
readonly expected: string,
readonly repositoryUrl: string,
readonly version: string,
}> = [
{
os: OperatingSystem.macOS,
}> = {
[OperatingSystem.macOS]: {
expected: 'https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.2/privacy.sexy-0.7.2.dmg',
repositoryUrl: 'https://github.com/undergroundwires/privacy.sexy.git',
version: '0.7.2',
},
{
os: OperatingSystem.Linux,
[OperatingSystem.Linux]: {
expected: 'https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.2/privacy.sexy-0.7.2.AppImage',
repositoryUrl: 'https://github.com/undergroundwires/privacy.sexy.git',
version: '0.7.2',
},
{
os: OperatingSystem.Windows,
[OperatingSystem.Windows]: {
expected: 'https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.2/privacy.sexy-Setup-0.7.2.exe',
repositoryUrl: 'https://github.com/undergroundwires/privacy.sexy.git',
version: '0.7.2',
},
];
for (const testCase of testCases) {
it(`should return the expected download URL for ${OperatingSystem[testCase.os]}`, () => {
};
AllSupportedOperatingSystems.forEach((operatingSystem) => {
it(`should return the expected download URL for ${OperatingSystem[operatingSystem]}`, () => {
// arrange
const {
expected, version, repositoryUrl, os,
} = testCase;
const { expected, version, repositoryUrl } = testCases[operatingSystem];
const sut = new ProjectInformationBuilder()
.withVersion(new VersionStub(version))
.withRepositoryUrl(repositoryUrl)
.build();
// act
const actual = sut.getDownloadUrl(os);
const actual = sut.getDownloadUrl(operatingSystem);
// assert
expect(actual).to.equal(expected);
});
}
});
describe('should throw an error when provided with an invalid operating system', () => {
// arrange
const sut = new ProjectInformationBuilder()

View File

@@ -0,0 +1,84 @@
import { describe, it, expect } from 'vitest';
import { AllSupportedOperatingSystems, SupportedOperatingSystem } from '@tests/shared/TestCases/SupportedOperatingSystems';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { generateOsTimestampedFileName } from '@/infrastructure/CodeRunner/FileNameGenerator';
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
describe('FileNameGenerator', () => {
describe('generateOsTimestampedFileName', () => {
it('generates correct prefix', () => {
// arrange
const expectedPrefix = 'run';
// act
const fileName = generateFileNamePartsForTesting();
// assert
expect(fileName.prefix).to.equal(expectedPrefix);
});
it('generates correct timestamp', () => {
// arrange
const currentDate = '2023-01-01T12:00:00.000Z';
const expectedTimestamp = '2023-01-01_12-00-00';
const date = new Date(currentDate);
// act
const fileName = generateFileNamePartsForTesting({ date });
// assert
expect(fileName.timestamp).to.equal(expectedTimestamp, formatAssertionMessage[
`Generated file name: ${fileName.generatedFileName}`
]);
});
describe('generates correct extension', () => {
const testScenarios: Record<SupportedOperatingSystem, string> = {
[OperatingSystem.Windows]: 'bat',
[OperatingSystem.Linux]: 'sh',
[OperatingSystem.macOS]: 'sh',
};
AllSupportedOperatingSystems.forEach((operatingSystem) => {
it(`on ${OperatingSystem[operatingSystem]}`, () => {
// arrange
const expectedExtension = testScenarios[operatingSystem];
// act
const fileName = generateFileNamePartsForTesting({ operatingSystem });
// assert
expect(fileName.extension).to.equal(expectedExtension, formatAssertionMessage[
`Generated file name: ${fileName.generatedFileName}`
]);
});
});
});
it('generates filename without extension for unknown OS', () => {
// arrange
const unknownOs = 'Unknown' as unknown as OperatingSystem;
// act
const fileName = generateFileNamePartsForTesting({ operatingSystem: unknownOs });
// assert
expect(fileName.extension).toBeUndefined();
});
});
});
interface TestFileNameComponents {
readonly prefix: string;
readonly timestamp: string;
readonly extension?: string;
readonly generatedFileName: string;
}
function generateFileNamePartsForTesting(testScenario?: {
operatingSystem?: OperatingSystem,
date?: Date,
}): TestFileNameComponents {
const operatingSystem = testScenario?.operatingSystem ?? OperatingSystem.Windows;
const date = testScenario?.date ?? new Date();
const fileName = generateOsTimestampedFileName(operatingSystem, date);
const pattern = /^(?<prefix>[^-]+)-(?<timestamp>[^.]+)(?:\.(?<extension>[^.]+))?$/; // prefix-timestamp.extension
const match = fileName.match(pattern);
if (!match?.groups?.prefix || !match?.groups?.timestamp) {
throw new Error(`Failed to parse prefix or timestamp: ${fileName}`);
}
return {
generatedFileName: fileName,
prefix: match.groups.prefix,
timestamp: match.groups.timestamp,
extension: match.groups.extension,
};
}

View File

@@ -1,7 +1,7 @@
import { describe, it, expect } from 'vitest';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { FileSystemOps, SystemOperations } from '@/infrastructure/CodeRunner/SystemOperations/SystemOperations';
import { TemporaryFileCodeRunner } from '@/infrastructure/CodeRunner/TemporaryFileCodeRunner';
import { FileNameGenerator, TemporaryFileCodeRunner } from '@/infrastructure/CodeRunner/TemporaryFileCodeRunner';
import { expectThrowsAsync } from '@tests/shared/Assertions/ExpectThrowsAsync';
import { SystemOperationsStub } from '@tests/unit/shared/Stubs/SystemOperationsStub';
import { OperatingSystemOpsStub } from '@tests/unit/shared/Stubs/OperatingSystemOpsStub';
@@ -9,9 +9,14 @@ import { LocationOpsStub } from '@tests/unit/shared/Stubs/LocationOpsStub';
import { FileSystemOpsStub } from '@tests/unit/shared/Stubs/FileSystemOpsStub';
import { CommandOpsStub } from '@tests/unit/shared/Stubs/CommandOpsStub';
import { FunctionKeys } from '@/TypeHelpers';
import { AllSupportedOperatingSystems, SupportedOperatingSystem } from '@tests/shared/TestCases/SupportedOperatingSystems';
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
import { LoggerStub } from '@tests/unit/shared/Stubs/LoggerStub';
import { Logger } from '@/application/Common/Log/Logger';
describe('TemporaryFileCodeRunner', () => {
describe('runCode', () => {
describe('directory creation', () => {
it('creates temporary directory recursively', async () => {
// arrange
const expectedDir = 'expected-dir';
@@ -20,7 +25,7 @@ describe('TemporaryFileCodeRunner', () => {
const folderName = 'privacy.sexy';
const temporaryDirName = 'tmp';
const filesystem = new FileSystemOpsStub();
const context = new TestContext()
const context = new CodeRunnerTestSetup()
.withSystemOperationsStub((ops) => ops
.withOperatingSystem(
new OperatingSystemOpsStub()
@@ -44,54 +49,133 @@ describe('TemporaryFileCodeRunner', () => {
expect(actualPath).to.equal(expectedDir);
expect(actualIsRecursive).to.equal(expectedIsRecursive);
});
it('creates a file with expected code and path', async () => {
});
describe('file creation', () => {
it('creates a file with expected code', async () => {
// arrange
const expectedCode = 'expected-code';
const expectedFilePath = 'expected-file-path';
const filesystem = new FileSystemOpsStub();
const extension = '.sh';
const expectedName = `run.${extension}`;
const folderName = 'privacy.sexy';
const temporaryDirName = 'tmp';
const context = new TestContext()
const context = new CodeRunnerTestSetup()
.withSystemOperationsStub((ops) => ops
.withOperatingSystem(
new OperatingSystemOpsStub()
.withTemporaryDirectoryResult(temporaryDirName),
)
.withLocation(
new LocationOpsStub()
.withJoinResult('folder', temporaryDirName, folderName)
.withJoinResult(expectedFilePath, 'folder', expectedName),
)
.withFileSystem(filesystem));
// act
await context
.withCode(expectedCode)
.withFolderName(folderName)
.withExtension(extension)
.runCode();
// assert
const calls = filesystem.callHistory.filter((call) => call.methodName === 'writeToFile');
expect(calls.length).to.equal(1);
const [actualFilePath, actualData] = calls[0].args;
expect(actualFilePath).to.equal(expectedFilePath);
const [, actualData] = calls[0].args;
expect(actualData).to.equal(expectedCode);
});
it('set file permissions as expected', async () => {
it('generates file name for correct operating system', async () => {
// arrange
const expectedOperatingSystem = OperatingSystem.macOS;
const calls = new Array<OperatingSystem>();
const fileNameGenerator: FileNameGenerator = (operatingSystem) => {
calls.push(operatingSystem);
return 'unimportant file name';
};
const context = new CodeRunnerTestSetup()
.withOs(expectedOperatingSystem)
.withFileNameGenerator(fileNameGenerator);
// act
await context.runCode();
// assert
expect(calls).to.have.lengthOf(1);
const [actualOperatingSystem] = calls;
expect(actualOperatingSystem).to.equal(expectedOperatingSystem);
});
it('creates file in expected directory', async () => {
// arrange
const pathSegmentSeparator = '/PATH-SEGMENT-SEPARATOR/';
const temporaryDirName = '/tmp';
const folderName = 'privacy.sexy';
const expectedDirectory = [temporaryDirName, folderName].join(pathSegmentSeparator);
const filesystem = new FileSystemOpsStub();
const context = new CodeRunnerTestSetup()
.withSystemOperationsStub((ops) => ops
.withOperatingSystem(
new OperatingSystemOpsStub()
.withTemporaryDirectoryResult(temporaryDirName),
)
.withLocation(
new LocationOpsStub().withDefaultSeparator(pathSegmentSeparator),
)
.withFileSystem(filesystem));
// act
await context
.withFolderName(folderName)
.runCode();
// assert
const calls = filesystem.callHistory.filter((call) => call.methodName === 'writeToFile');
expect(calls.length).to.equal(1);
const [actualFilePath] = calls[0].args;
const actualDirectory = actualFilePath
.split(pathSegmentSeparator)
.slice(0, -1)
.join(pathSegmentSeparator);
expect(actualDirectory).to.equal(expectedDirectory, formatAssertionMessage([
`Actual file path: ${actualFilePath}`,
]));
});
it('creates file with expected file name', async () => {
// arrange
const pathSegmentSeparator = '/PATH-SEGMENT-SEPARATOR/';
const filesystem = new FileSystemOpsStub();
const expectedFileName = 'expected-script-file-name';
const context = new CodeRunnerTestSetup()
.withFileNameGenerator(() => expectedFileName)
.withSystemOperationsStub((ops) => ops
.withFileSystem(filesystem)
.withLocation(new LocationOpsStub().withDefaultSeparator(pathSegmentSeparator)));
// act
await context.runCode();
// assert
const calls = filesystem.callHistory.filter((call) => call.methodName === 'writeToFile');
expect(calls.length).to.equal(1);
const [actualFilePath] = calls[0].args;
const actualFileName = actualFilePath
.split(pathSegmentSeparator)
.pop();
expect(actualFileName).to.equal(actualFileName, formatAssertionMessage([
`Actual file path: ${actualFilePath}`,
]));
});
});
describe('file permissions', () => {
it('sets correct permissions', async () => {
// arrange
const expectedMode = '755';
const expectedFilePath = 'expected-file-path';
const filesystem = new FileSystemOpsStub();
const context = new CodeRunnerTestSetup()
.withSystemOperationsStub((ops) => ops.withFileSystem(filesystem));
// act
await context.runCode();
// assert
const calls = filesystem.callHistory.filter((call) => call.methodName === 'setFilePermissions');
expect(calls.length).to.equal(1);
const [, actualMode] = calls[0].args;
expect(actualMode).to.equal(expectedMode);
});
it('sets correct permissions on correct file', async () => {
// arrange
const expectedFilePath = 'expected-file-path';
const filesystem = new FileSystemOpsStub();
const extension = '.sh';
const expectedName = `run.${extension}`;
const folderName = 'privacy.sexy';
const temporaryDirName = 'tmp';
const context = new TestContext()
const context = new CodeRunnerTestSetup()
.withSystemOperationsStub((ops) => ops
.withOperatingSystem(
new OperatingSystemOpsStub()
@@ -107,41 +191,32 @@ describe('TemporaryFileCodeRunner', () => {
// act
await context
.withFolderName(folderName)
.withExtension(extension)
.withFileNameGenerator(() => expectedName)
.runCode();
// assert
const calls = filesystem.callHistory.filter((call) => call.methodName === 'setFilePermissions');
expect(calls.length).to.equal(1);
const [actualFilePath, actualMode] = calls[0].args;
const [actualFilePath] = calls[0].args;
expect(actualFilePath).to.equal(expectedFilePath);
expect(actualMode).to.equal(expectedMode);
});
describe('executes as expected', () => {
});
describe('file execution', () => {
describe('executes correct command', () => {
// arrange
const filePath = 'expected-file-path';
interface ExecutionTestCase {
readonly givenOs: OperatingSystem;
readonly expectedCommand: string;
}
const testData: readonly ExecutionTestCase[] = [
{
givenOs: OperatingSystem.Windows,
expectedCommand: filePath,
},
{
givenOs: OperatingSystem.macOS,
expectedCommand: `open -a Terminal.app ${filePath}`,
},
{
givenOs: OperatingSystem.Linux,
expectedCommand: `x-terminal-emulator -e '${filePath}'`,
},
];
for (const { givenOs, expectedCommand } of testData) {
it(`returns ${expectedCommand} on ${OperatingSystem[givenOs]}`, async () => {
const filePath = 'executed-file-path';
const testScenarios: Record<SupportedOperatingSystem, string> = {
[OperatingSystem.Windows]: filePath,
[OperatingSystem.macOS]: `open -a Terminal.app ${filePath}`,
[OperatingSystem.Linux]: `x-terminal-emulator -e '${filePath}'`,
};
AllSupportedOperatingSystems.forEach((operatingSystem) => {
it(`returns correctly for ${OperatingSystem[operatingSystem]}`, async () => {
// arrange
const expectedCommand = testScenarios[operatingSystem];
const command = new CommandOpsStub();
const context = new TestContext()
const context = new CodeRunnerTestSetup()
.withFileNameGenerator(() => filePath)
.withSystemOperationsStub((ops) => ops
.withLocation(
new LocationOpsStub()
@@ -151,7 +226,7 @@ describe('TemporaryFileCodeRunner', () => {
// act
await context
.withOs(givenOs)
.withOs(operatingSystem)
.runCode();
// assert
@@ -160,7 +235,7 @@ describe('TemporaryFileCodeRunner', () => {
const [actualCommand] = calls[0].args;
expect(actualCommand).to.equal(expectedCommand);
});
}
});
});
it('runs in expected order', async () => { // verifies correct `async`, `await` usage.
const expectedOrder: readonly FunctionKeys<FileSystemOps>[] = [
@@ -169,7 +244,7 @@ describe('TemporaryFileCodeRunner', () => {
'setFilePermissions',
];
const fileSystem = new FileSystemOpsStub();
const context = new TestContext()
const context = new CodeRunnerTestSetup()
.withSystemOperationsStub((ops) => ops
.withFileSystem(fileSystem));
@@ -182,6 +257,7 @@ describe('TemporaryFileCodeRunner', () => {
.filter((command) => expectedOrder.includes(command));
expect(expectedOrder).to.deep.equal(actualOrder);
});
});
describe('throws with invalid OS', () => {
const testScenarios: ReadonlyArray<{
readonly description: string;
@@ -200,7 +276,7 @@ describe('TemporaryFileCodeRunner', () => {
testScenarios.forEach(({ description, invalidOs, expectedError }) => {
it(description, async () => {
// arrange
const context = new TestContext()
const context = new CodeRunnerTestSetup()
.withOs(invalidOs);
// act
const act = async () => { await context.runCode(); };
@@ -212,20 +288,26 @@ describe('TemporaryFileCodeRunner', () => {
});
});
class TestContext {
private code = 'code';
class CodeRunnerTestSetup {
private code = `[${CodeRunnerTestSetup.name}]code`;
private folderName = 'folderName';
private folderName = `[${CodeRunnerTestSetup.name}]folderName`;
private fileExtension = 'fileExtension';
private fileNameGenerator: FileNameGenerator = () => `[${CodeRunnerTestSetup.name}]file-name-stub`;
private os: OperatingSystem = OperatingSystem.Windows;
private systemOperations: SystemOperations = new SystemOperationsStub();
private logger: Logger = new LoggerStub();
public async runCode(): Promise<void> {
const runner = new TemporaryFileCodeRunner(this.systemOperations);
await runner.runCode(this.code, this.folderName, this.fileExtension, this.os);
const runner = new TemporaryFileCodeRunner(
this.systemOperations,
this.fileNameGenerator,
this.logger,
);
await runner.runCode(this.code, this.folderName, this.os);
}
public withSystemOperations(
@@ -257,8 +339,8 @@ class TestContext {
return this;
}
public withExtension(fileExtension: string): this {
this.fileExtension = fileExtension;
public withFileNameGenerator(fileNameGenerator: FileNameGenerator): this {
this.fileNameGenerator = fileNameGenerator;
return this;
}
}

View File

@@ -3,15 +3,15 @@ import { OperatingSystem } from '@/domain/OperatingSystem';
import { getInstructions } from '@/presentation/components/Code/CodeButtons/Save/Instructions/InstructionListDataFactory';
import { getEnumValues } from '@/application/Common/Enum';
import { InstructionsBuilder } from '@/presentation/components/Code/CodeButtons/Save/Instructions/Data/InstructionsBuilder';
import { AllSupportedOperatingSystems } from '@tests/shared/TestCases/SupportedOperatingSystems';
describe('InstructionListDataFactory', () => {
const supportedOsList = [OperatingSystem.macOS, OperatingSystem.Linux];
describe('getInstructions', () => {
it('returns expected if os is supported', () => {
// arrange
const fileName = 'test.file';
// act
const actualResults = supportedOsList.map((os) => getInstructions(os, fileName));
const actualResults = AllSupportedOperatingSystems.map((os) => getInstructions(os, fileName));
// assert
expect(actualResults.every((result) => result instanceof InstructionsBuilder));
});
@@ -20,7 +20,7 @@ describe('InstructionListDataFactory', () => {
const expected = undefined;
const fileName = 'test.file';
const unsupportedOses = getEnumValues(OperatingSystem)
.filter((value) => !supportedOsList.includes(value));
.filter((value) => !AllSupportedOperatingSystems.includes(value));
// act
const actualResults = unsupportedOses.map((os) => getInstructions(os, fileName));
// assert

View File

@@ -8,6 +8,8 @@ export class LocationOpsStub
private scenarios = new Map<string, string>();
private defaultSeparator = `/[${LocationOpsStub.name}]PATH-SEGMENT-SEPARATOR/`;
public withJoinResult(returnValue: string, ...paths: string[]): this {
this.scenarios.set(LocationOpsStub.getScenarioKey(paths), returnValue);
return this;
@@ -19,6 +21,11 @@ export class LocationOpsStub
return this;
}
public withDefaultSeparator(defaultSeparator: string): this {
this.defaultSeparator = defaultSeparator;
return this;
}
public combinePaths(...pathSegments: string[]): string {
this.registerMethodCall({
methodName: 'combinePaths',
@@ -33,7 +40,7 @@ export class LocationOpsStub
if (foundScenario) {
return foundScenario;
}
return pathSegments.join('/PATH-SEGMENT-SEPARATOR/');
return pathSegments.join(this.defaultSeparator);
}
private static getScenarioKey(paths: string[]): string {