Skip to content

Commit b538ebb

Browse files
CHANGE: @W-19980062@: Switch to vitest and make tests more robust (#1911)
1 parent 4eb034e commit b538ebb

24 files changed

+6505
-7859
lines changed

package-lock.json

Lines changed: 6021 additions & 7532 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 6 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"@oclif/core": "3.27.0",
99
"@salesforce/code-analyzer-core": "0.38.1",
1010
"@salesforce/code-analyzer-engine-api": "0.30.0",
11-
"@salesforce/code-analyzer-eslint-engine": "0.34.0",
11+
"@salesforce/code-analyzer-eslint-engine": "0.35.0",
1212
"@salesforce/code-analyzer-flow-engine": "0.27.0",
1313
"@salesforce/code-analyzer-pmd-engine": "0.31.0",
1414
"@salesforce/code-analyzer-regex-engine": "0.28.0",
@@ -31,18 +31,16 @@
3131
"@eslint/js": "^9.35.0",
3232
"@oclif/plugin-help": "^6.2.33",
3333
"@salesforce/cli-plugins-testkit": "^5.3.41",
34-
"@types/jest": "^30.0.0",
3534
"@types/tmp": "^0.2.6",
35+
"@vitest/coverage-istanbul": "^3.2.4",
3636
"eslint": "^9.35.0",
3737
"eslint-plugin-sf-plugin": "^1.20.32",
3838
"husky": "^9.1.7",
39-
"jest": "^30.1.3",
40-
"jest-junit": "^16.0.0",
4139
"oclif": "^4.22.22",
42-
"tmp": "^0.2.5",
43-
"ts-jest": "^29.4.4",
4440
"typescript": "^5.9.2",
45-
"typescript-eslint": "^8.44.0"
41+
"typescript-eslint": "^8.44.0",
42+
"vite": "^7.1.5",
43+
"vitest": "^3.2.4"
4644
},
4745
"engines": {
4846
"node": ">=20.0.0"
@@ -76,63 +74,14 @@
7674
"topicSeparator": " ",
7775
"flexibleTaxonomy": true
7876
},
79-
"jest": {
80-
"testTimeout": 60000,
81-
"collectCoverageFrom": [
82-
"src/**/*.ts",
83-
"!src/index.ts",
84-
"!src/lib/Display.ts"
85-
],
86-
"coverageReporters": [
87-
"lcov",
88-
"json",
89-
"text"
90-
],
91-
"coverageThreshold": {
92-
"global": {
93-
"branches": 80,
94-
"functions": 80,
95-
"lines": 80,
96-
"statements": 80
97-
}
98-
},
99-
"preset": "ts-jest",
100-
"reporters": [
101-
"default",
102-
[
103-
"jest-junit",
104-
{
105-
"outputDirectory": "reports",
106-
"outputName": "report.xml"
107-
}
108-
],
109-
[
110-
"github-actions",
111-
{
112-
"silent": false
113-
},
114-
"summary"
115-
]
116-
],
117-
"setupFiles": ["./test/setup-jest.ts"],
118-
"testEnvironment": "node",
119-
"testMatch": [
120-
"<rootDir>/test/**/*.test.ts"
121-
],
122-
"testPathIgnorePatterns": [
123-
"<rootDir>/node_modules/",
124-
"<rootDir>/lib/",
125-
"<rootDir>/dist/"
126-
]
127-
},
12877
"repository": "forcedotcom/code-analyzer",
12978
"scripts": {
13079
"build": "tsc --build tsconfig.json",
13180
"prepack": "rm -rf lib && tsc --build tsconfig.json && oclif manifest && oclif readme && npm shrinkwrap",
13281
"postpack": "rm -f oclif.manifest.json oclif.lock npm-shrinkwrap.json",
13382
"lint": "eslint ./src --max-warnings 0",
13483
"version": "oclif readme && git add README.md",
135-
"test": "tsc --build ./test/tsconfig.json --noEmit && jest --coverage",
84+
"test": "tsc --build ./test/tsconfig.json --noEmit && vitest run --coverage",
13685
"showcoverage": "open ./coverage/lcov-report/index.html",
13786
"prepare": "husky"
13887
}

src/lib/utils/FileUtil.ts

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,39 @@
11
import * as fs from 'node:fs';
22

3-
export async function exists(filename: string): Promise<boolean> {
4-
try {
5-
await fs.promises.access(filename, fs.constants.F_OK);
6-
return true;
7-
} catch (_e) {
8-
return false;
9-
}
3+
export interface FileSystem {
4+
exists(file: string): Promise<boolean>
5+
6+
writeFileSync(file: string, contents: string): void
7+
8+
createWriteStream(file: string): Promise<WriteStream>;
109
}
10+
11+
export interface WriteStream {
12+
write(content: string): void
13+
end(): void
14+
}
15+
16+
export class RealFileSystem implements FileSystem {
17+
async exists(file: string): Promise<boolean> {
18+
try {
19+
await fs.promises.access(file, fs.constants.F_OK);
20+
return true;
21+
} catch (_e) {
22+
return false;
23+
}
24+
}
25+
26+
writeFileSync(file: string, contents: string): void {
27+
fs.writeFileSync(file, contents);
28+
}
29+
30+
createWriteStream(file: string): Promise<WriteStream> {
31+
// We return a promise so that we can await for the stream to be opened
32+
// if we want before proceeding with writes.
33+
return new Promise((resolve, reject) => {
34+
const stream: fs.WriteStream = fs.createWriteStream(file);
35+
stream.once('open', () => resolve(stream));
36+
stream.once('error', (err) => reject(err));
37+
});
38+
}
39+
}

src/lib/utils/StylingUtil.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,30 @@
1-
import ansis from 'ansis';
1+
import {Ansis} from 'ansis';
22

33
/**
44
* For now, the styling methods only accept objects if all of their keys correspond to string values. This puts the
55
* burden of formatting non-string properties on the caller.
66
*/
77
type Styleable = null | undefined | {[key: string]: string|string[]};
88

9+
export const __ANSIS = {
10+
// The default Ansis instance uses environment variables to decide whether to add decoration or not.
11+
// We use this holder so that in testing we can swap out this instance with new Ansis(3) to force color in order
12+
// to make the tests more robust.
13+
instance: new Ansis() // This auto detects whether to use color or not
14+
}
15+
916
export function toStyledHeaderAndBody(header: string, body: Styleable, keys?: string[]): string {
1017
const styledHeader: string = toStyledHeader(header);
1118
const styledBody: string = indent(toStyledPropertyList(body, keys));
1219
return `${styledHeader}\n${styledBody}`;
1320
}
1421

1522
export function toStyledHeader(header: string): string {
16-
return `${ansis.dim('===')} ${ansis.bold(header)}`;
23+
return `${__ANSIS.instance.dim('===')} ${__ANSIS.instance.bold(header)}`;
1724
}
1825

1926
export function makeGrey(str: string): string {
20-
return ansis.dim(str);
27+
return __ANSIS.instance.dim(str);
2128
}
2229

2330
export function toStyledPropertyList(body: Styleable, selectedKeys?: string[]): string {
@@ -28,7 +35,7 @@ export function toStyledPropertyList(body: Styleable, selectedKeys?: string[]):
2835
const longestKeyLength = Math.max(...keysToPrint.map(k => k.length));
2936

3037
const styleProperty = (key: string, value: string|string[]): string => {
31-
const keyPortion = `${ansis.blue(key)}:`;
38+
const keyPortion = `${__ANSIS.instance.blue(key)}:`;
3239
const keyValueGap = ' '.repeat(longestKeyLength - key.length + 1);
3340
if (typeof value === 'string') {
3441
const valuePortion = value.replace('\n', `\n${' '.repeat(longestKeyLength + 2)}`);
@@ -45,4 +52,4 @@ export function toStyledPropertyList(body: Styleable, selectedKeys?: string[]):
4552

4653
export function indent(text: string, indentLength: number = 4): string {
4754
return text.replace(/^.+/gm, m => m.length > 0 ? ' '.repeat(indentLength) + m : m);
48-
}
55+
}

src/lib/writers/ConfigWriter.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import path from 'node:path';
2-
import fs from 'node:fs';
1+
import * as path from 'node:path';
32
import {ConfigModel, OutputFormat} from '../models/ConfigModel';
43
import {BundleName, getMessage} from '../messages';
54
import {Display} from '../Display';
6-
import {exists} from '../utils/FileUtil';
5+
import {FileSystem, RealFileSystem} from '../utils/FileUtil';
76

87
export interface ConfigWriter {
98
write(model: ConfigModel): Promise<boolean>;
@@ -13,27 +12,29 @@ export class ConfigFileWriter implements ConfigWriter {
1312
private readonly file: string;
1413
private readonly format: OutputFormat;
1514
private readonly display: Display;
15+
private readonly fileSystem: FileSystem;
1616

17-
private constructor(file: string, format: OutputFormat, display: Display) {
17+
private constructor(file: string, format: OutputFormat, display: Display, fileSystem: FileSystem) {
1818
this.file = file;
1919
this.format = format;
2020
this.display = display;
21+
this.fileSystem = fileSystem;
2122
}
2223

2324
public async write(model: ConfigModel): Promise<boolean> {
2425
// Only write to the file if it doesn't already exist, or if the user confirms that they want to overwrite it.
25-
if (!(await exists(this.file)) || await this.display.confirm(getMessage(BundleName.ConfigWriter, 'prompt.overwrite-existing-file', [this.file]))) {
26-
fs.writeFileSync(this.file, model.toFormattedOutput(this.format));
26+
if (!(await this.fileSystem.exists(this.file)) || await this.display.confirm(getMessage(BundleName.ConfigWriter, 'prompt.overwrite-existing-file', [this.file]))) {
27+
this.fileSystem.writeFileSync(this.file, model.toFormattedOutput(this.format));
2728
return true;
2829
} else {
2930
return false;
3031
}
3132
}
3233

33-
public static fromFile(file: string, display: Display): ConfigFileWriter {
34+
public static fromFile(file: string, display: Display, fileSystem: FileSystem = new RealFileSystem()): ConfigFileWriter {
3435
const ext = path.extname(file).toLowerCase();
3536
if (ext === '.yaml' || ext === '.yml') {
36-
return new ConfigFileWriter(file, OutputFormat.RAW_YAML, display);
37+
return new ConfigFileWriter(file, OutputFormat.RAW_YAML, display, fileSystem);
3738
} else {
3839
throw new Error(getMessage(BundleName.ConfigWriter, 'error.unrecognized-file-format', [file]));
3940
}

src/lib/writers/LogWriter.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as path from 'node:path';
2-
import * as fs from 'node:fs/promises';
32
import {CodeAnalyzerConfig} from '@salesforce/code-analyzer-core';
43
import {Clock, RealClock, formatToDateTimeString} from '../utils/DateTimeUtils';
4+
import {FileSystem, RealFileSystem, WriteStream} from '../utils/FileUtil';
55

66
export interface LogWriter {
77
writeToLog(message: string): void;
@@ -10,10 +10,10 @@ export interface LogWriter {
1010
}
1111

1212
export class LogFileWriter implements LogWriter {
13-
private readonly writeStream: NodeJS.WritableStream;
13+
private readonly writeStream: WriteStream;
1414
private readonly destination: string;
1515

16-
private constructor(writeStream: NodeJS.WritableStream, destination: string) {
16+
private constructor(writeStream: WriteStream, destination: string) {
1717
this.writeStream = writeStream;
1818
this.destination = destination;
1919
}
@@ -30,13 +30,13 @@ export class LogFileWriter implements LogWriter {
3030
this.writeStream.end();
3131
}
3232

33-
public static async fromConfig(config: CodeAnalyzerConfig, clock: Clock = new RealClock()): Promise<LogFileWriter> {
33+
public static async fromConfig(config: CodeAnalyzerConfig, clock: Clock = new RealClock(), fileSystem: FileSystem = new RealFileSystem()): Promise<LogFileWriter> {
3434
const logFolder = config.getLogFolder();
35+
3536
// Use the current timestamp to make sure each transaction has a unique logfile. If we want to reuse logfiles,
3637
// or just have one running logfile, we can change this.
3738
const logFile = path.join(logFolder, `sfca-${formatToDateTimeString(clock.now())}.log`);
38-
// 'w' flag causes the file to be created if it doesn't already exist.
39-
const fh = await fs.open(logFile, 'w');
40-
return new LogFileWriter(fh.createWriteStream({}), logFile);
39+
40+
return new LogFileWriter(await fileSystem.createWriteStream(logFile), logFile);
4141
}
4242
}

src/lib/writers/ResultsWriter.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import * as fs from 'node:fs';
2-
import path from 'node:path';
1+
import * as path from 'node:path';
32
import {OutputFormat, RunResults} from '@salesforce/code-analyzer-core';
43
import {BundleName, getMessage} from '../messages';
4+
import {FileSystem, RealFileSystem} from '../utils/FileUtil';
55

66
export interface ResultsWriter {
77
write(results: RunResults): void;
@@ -18,16 +18,17 @@ export class CompositeResultsWriter implements ResultsWriter {
1818
this.writers.forEach(w => w.write(results));
1919
}
2020

21-
public static fromFiles(files: string[]): CompositeResultsWriter {
22-
return new CompositeResultsWriter(files.map(f => new ResultsFileWriter(f)));
21+
public static fromFiles(files: string[], fileSystem: FileSystem = new RealFileSystem()): CompositeResultsWriter {
22+
return new CompositeResultsWriter(files.map(f => new ResultsFileWriter(f, fileSystem)));
2323
}
2424
}
2525

2626
export class ResultsFileWriter implements ResultsWriter {
2727
private readonly file: string;
2828
private readonly format: OutputFormat;
29+
private readonly fileSystem: FileSystem;
2930

30-
public constructor(file: string) {
31+
public constructor(file: string, fileSystem: FileSystem = new RealFileSystem()) {
3132
this.file = file;
3233
const ext = path.extname(file).toLowerCase();
3334
if (ext === '.csv') {
@@ -44,9 +45,10 @@ export class ResultsFileWriter implements ResultsWriter {
4445
} else {
4546
throw new Error(getMessage(BundleName.ResultsWriter, 'error.unrecognized-file-format', [file]));
4647
}
48+
this.fileSystem = fileSystem;
4749
}
4850

4951
public write(results: RunResults): void {
50-
fs.writeFileSync(this.file, results.toFormattedOutput(this.format));
52+
this.fileSystem.writeFileSync(this.file, results.toFormattedOutput(this.format));
5153
}
5254
}

src/lib/writers/RulesWriter.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
import * as fs from 'node:fs';
1+
import * as path from "path";
22
import { OutputFormat, RuleSelection } from "@salesforce/code-analyzer-core";
3-
import path from "path";
43
import { BundleName, getMessage } from "../messages";
4+
import { FileSystem, RealFileSystem } from "../utils/FileUtil";
55

66
export interface RulesWriter {
77
write(rules: RuleSelection): void;
88
}
99

10-
1110
export class CompositeRulesWriter implements RulesWriter {
1211
private readonly writers: RulesWriter[] = [];
1312

@@ -19,30 +18,33 @@ export class CompositeRulesWriter implements RulesWriter {
1918
this.writers.forEach(w => w.write(rules));
2019
}
2120

22-
public static fromFiles(files: string[]): CompositeRulesWriter {
23-
return new CompositeRulesWriter(files.map(f => new RulesFileWriter(f)));
21+
public static fromFiles(files: string[], fileSystem: FileSystem = new RealFileSystem()): CompositeRulesWriter {
22+
return new CompositeRulesWriter(files.map(f => new RulesFileWriter(f, fileSystem)));
2423
}
2524
}
2625

2726
export class RulesFileWriter implements RulesWriter {
2827
private readonly file: string;
2928
private readonly format: OutputFormat;
29+
private readonly fileSystem: FileSystem;
3030

31-
public constructor(file: string) {
31+
public constructor(file: string, fileSystem: FileSystem = new RealFileSystem()) {
3232
this.file = file;
33-
const ext = path.extname(file).toLowerCase();
3433

34+
const ext = path.extname(file).toLowerCase();
3535
if (ext === '.json') {
3636
this.format = OutputFormat.JSON;
3737
} else if (ext === '.csv') {
3838
this.format = OutputFormat.CSV;
3939
} else {
4040
throw new Error(getMessage(BundleName.RulesWriter, 'error.unrecognized-file-format', [file]));
4141
}
42+
43+
this.fileSystem = fileSystem;
4244
}
4345

4446
public write(ruleSelection: RuleSelection): void {
4547
const contents = ruleSelection.toFormattedOutput(this.format);
46-
fs.writeFileSync(this.file, contents);
48+
this.fileSystem.writeFileSync(this.file, contents);
4749
}
4850
}

0 commit comments

Comments
 (0)