Skip to content

Commit

Permalink
chore: migrate from TestFile to test suite
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman committed Mar 7, 2024
1 parent f2a53b3 commit 6e4cdf0
Show file tree
Hide file tree
Showing 12 changed files with 137 additions and 197 deletions.
29 changes: 21 additions & 8 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import * as reporterTypes from './reporter';
import { ReusedBrowser } from './reusedBrowser';
import { SettingsModel } from './settingsModel';
import { SettingsView } from './settingsView';
import { TestModel, TestProject } from './testModel';
import { TestModel, TestProject, projectFiles } from './testModel';
import { TestTree } from './testTree';
import { NodeJSNotFoundError, ansiToHtml } from './utils';
import * as vscodeTypes from './vscodeTypes';
Expand Down Expand Up @@ -310,7 +310,7 @@ export class Extension implements RunHooks {
for (const model of this._models) {
for (const project of model.allProjects().values()) {
this._createRunProfile(project, existingProfiles);
this._workspaceObserver.addWatchFolder(project.testDir);
this._workspaceObserver.addWatchFolder(project.project.testDir);
}
}
for (const [id, profile] of existingProfiles) {
Expand Down Expand Up @@ -369,19 +369,31 @@ export class Extension implements RunHooks {
const projectPrefix = project.name ? `${project.name} — ` : '';
const keyPrefix = configFile + ':' + project.name;
let runProfile = existingProfiles.get(keyPrefix + ':run');
const projectTag = this._testTree.projectTag(project);
const isDefault = false;
const supportsContinuousRun = this._settingsModel.allowWatchingFiles.get();
if (!runProfile)
runProfile = this._testController.createRunProfile(`${projectPrefix}${folderName}${path.sep}${configName}`, this._vscode.TestRunProfileKind.Run, this._scheduleTestRunRequest.bind(this, configFile, project.name, false), isDefault, projectTag, supportsContinuousRun);
runProfile = this._testController.createRunProfile(`${projectPrefix}${folderName}${path.sep}${configName}`, this._vscode.TestRunProfileKind.Run, this._scheduleTestRunRequest.bind(this, configFile, project.name, false), isDefault, undefined, supportsContinuousRun);
this._runProfiles.set(keyPrefix + ':run', runProfile);
let debugProfile = existingProfiles.get(keyPrefix + ':debug');
if (!debugProfile)
debugProfile = this._testController.createRunProfile(`${projectPrefix}${folderName}${path.sep}${configName}`, this._vscode.TestRunProfileKind.Debug, this._scheduleTestRunRequest.bind(this, configFile, project.name, true), isDefault, projectTag, supportsContinuousRun);
debugProfile = this._testController.createRunProfile(`${projectPrefix}${folderName}${path.sep}${configName}`, this._vscode.TestRunProfileKind.Debug, this._scheduleTestRunRequest.bind(this, configFile, project.name, true), isDefault, undefined, supportsContinuousRun);
this._runProfiles.set(keyPrefix + ':debug', debugProfile);

runProfile.onDidChangeDefault(enabled => project.model.setProjectEnabled(project, enabled));
debugProfile.onDidChangeDefault(enabled => project.model.setProjectEnabled(project, enabled));
// Run profile has the current isEnabled value as per vscode.
project.isEnabled = runProfile.isDefault;

runProfile.onDidChangeDefault(enabled => {
if (project.isEnabled === enabled)
return;
project.model.setProjectEnabled(project, enabled);
this._updateVisibleEditorItems();
});
debugProfile.onDidChangeDefault(enabled => {
if (project.isEnabled === enabled)
return;
project.model.setProjectEnabled(project, enabled);
this._updateVisibleEditorItems();
});
}

private _scheduleTestRunRequest(configFile: string, projectName: string, isDebug: boolean, request: vscodeTypes.TestRunRequest, cancellationToken?: vscodeTypes.CancellationToken) {
Expand Down Expand Up @@ -526,7 +538,8 @@ located next to Run / Debug Tests toolbar buttons.`);
for (const item of items) {
const itemFsPath = item.uri!.fsPath;
const projectsWithFile = projects.filter(project => {
for (const file of project.files.keys()) {
const files = projectFiles(project);
for (const file of files.keys()) {
if (file.startsWith(itemFsPath))
return true;
}
Expand Down
4 changes: 2 additions & 2 deletions src/playwrightTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ export class PlaywrightTest {
return;
const configFolder = path.dirname(config.configFile);
const configFile = path.basename(config.configFile);
const escapedLocations = locations.map(escapeRegex);
const escapedLocations = locations.map(escapeRegex).sort();
const args = [];
if (mode === 'list')
args.push('--list', '--reporter=null');
Expand All @@ -206,7 +206,7 @@ export class PlaywrightTest {

{
// For tests.
const relativeLocations = locations.map(f => path.relative(configFolder, f)).map(escapeRegex);
const relativeLocations = escapedLocations.map(f => path.relative(configFolder, f));
this._log(`${escapeRegex(path.relative(config.workspaceFolder, configFolder))}> playwright test -c ${configFile}${args.length ? ' ' + args.join(' ') : ''}${relativeLocations.length ? ' ' + relativeLocations.join(' ') : ''}`);
}
const allArgs = [config.cli, 'test',
Expand Down
2 changes: 1 addition & 1 deletion src/reusedBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ export class ReusedBrowser implements vscodeTypes.Disposable {
return;
let file;
for (let i = 1; i < 100; ++i) {
file = path.join(project.testDir, `test-${i}.spec.ts`);
file = path.join(project.project.testDir, `test-${i}.spec.ts`);
if (fs.existsSync(file))
continue;
break;
Expand Down
197 changes: 79 additions & 118 deletions src/testModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,49 +20,15 @@ import * as vscodeTypes from './vscodeTypes';
import { resolveSourceMap } from './utils';
import { ProjectConfigWithFiles } from './listTests';
import * as reporterTypes from './reporter';
import { TeleSuite } from './upstream/teleReceiver';

export type TestEntry = reporterTypes.TestCase | reporterTypes.Suite;

/**
* This class builds the Playwright Test model in Playwright terms.
* - TestModel maps to the Playwright config
* - TestProject maps to the Playwright project
* - TestFiles belong to projects and contain test entries.
*
* A single test in the source code, and a single test in VS Code UI can correspond to multiple entries
* in different configs / projects. TestTree will perform model -> UI mapping and will represent
* them as a single entity.
*/
export class TestFile {
readonly project: TestProject;
readonly file: string;
private _entries: TestEntry[] | undefined;
private _revision = 0;

constructor(project: TestProject, file: string) {
this.project = project;
this.file = file;
}

entries(): TestEntry[] | undefined {
return this._entries;
}

setEntries(entries: TestEntry[]) {
++this._revision;
this._entries = entries;
}

revision(): number {
return this._revision;
}
}

export type TestProject = {
name: string;
testDir: string;
model: TestModel;
files: Map<string, TestFile>;
name: string;
suite: reporterTypes.Suite;
project: reporterTypes.FullProject;
isEnabled: boolean;
};

Expand Down Expand Up @@ -102,7 +68,8 @@ export class TestModel {
enabledFiles(): string[] {
const result: string[] = [];
for (const project of this.enabledProjects()) {
for (const file of project.files.keys())
const files = projectFiles(project);
for (const file of files.keys())
result.push(file);
}
return result;
Expand Down Expand Up @@ -140,82 +107,65 @@ export class TestModel {
}

private _createProject(projectReport: ProjectConfigWithFiles): TestProject {
const projectSuite = new TeleSuite(projectReport.name, 'project');
projectSuite._project = {
dependencies: [],
grep: '.*',
grepInvert: null,
metadata: {},
name: projectReport.name,
outputDir: '',
repeatEach: 0,
retries: 0,
snapshotDir: '',
testDir: projectReport.testDir,
testIgnore: [],
testMatch: '.*',
timeout: 0,
use: projectReport.use,
};
const project: TestProject = {
model: this,
...projectReport,
files: new Map(),
isEnabled: true,
name: projectReport.name,
suite: projectSuite,
project: projectSuite._project,
isEnabled: false,
};
this._projects.set(project.name, project);
return project;
}

private _updateProject(project: TestProject, projectReport: ProjectConfigWithFiles) {
const filesToKeep = new Set<string>();
const files = projectFiles(project);
for (const file of projectReport.files) {
filesToKeep.add(file);
const testFile = project.files.get(file);
if (!testFile)
this._createFile(project, file);
const testFile = files.get(file);
if (!testFile) {
const testFile = new TeleSuite(file, 'file');
testFile.location = { file, line: 0, column: 0 };
files.set(file, testFile);
}
}

for (const file of project.files.keys()) {
for (const file of files.keys()) {
if (!filesToKeep.has(file))
project.files.delete(file);
files.delete(file);
}
}

private _createFile(project: TestProject, file: string): TestFile {
const testFile = new TestFile(project, file);
project.files.set(file, testFile);
return testFile;
project.suite.suites = [...files.values()];
}

async workspaceChanged(change: WorkspaceChange) {
let modelChanged = false;
// Translate source maps from files to sources.
change.changed = this._mapFilesToSources(change.changed);
change.created = this._mapFilesToSources(change.created);
change.deleted = this._mapFilesToSources(change.deleted);
const testDirs = [...new Set([...this._projects.values()].map(p => p.project.testDir))];

if (change.deleted.size) {
for (const project of this._projects.values()) {
for (const file of change.deleted) {
if (project.files.has(file)) {
project.files.delete(file);
modelChanged = true;
}
}
}
}

if (change.created.size) {
let hasMatchingFiles = false;
for (const project of this._projects.values()) {
for (const file of change.created) {
if (file.startsWith(project.testDir))
hasMatchingFiles = true;
}
}
if (hasMatchingFiles)
await this.listFiles();
}
const changed = this._mapFilesToSources(testDirs, change.changed);
const created = this._mapFilesToSources(testDirs, change.created);
const deleted = this._mapFilesToSources(testDirs, change.deleted);

if (change.changed.size) {
const filesToLoad = new Set<string>();
for (const project of this._projects.values()) {
for (const file of change.changed) {
const testFile = project.files.get(file);
if (!testFile || !testFile.entries())
continue;
filesToLoad.add(file);
}
}
if (filesToLoad.size)
await this.listTests([...filesToLoad]);
}
if (modelChanged)
this._didUpdate.fire();
if (created.length || deleted.length)
await this.listFiles();
if (changed.length)
await this.listTests(changed);
}

async listTests(files: string[]): Promise<reporterTypes.TestError[]> {
Expand All @@ -224,23 +174,23 @@ export class TestModel {
return errors;
}

private _updateProjects(projectSuites: reporterTypes.Suite[], requestedFiles: string[]) {
private _updateProjects(newProjectSuites: reporterTypes.Suite[], requestedFiles: string[]) {
for (const [projectName, project] of this._projects) {
const projectSuite = projectSuites.find(e => e.project()!.name === projectName);
const filesToDelete = new Set(requestedFiles);
for (const fileSuite of projectSuite?.suites || []) {
filesToDelete.delete(fileSuite.location!.file);
const file = project.files.get(fileSuite.location!.file);
if (!file)
continue;
file.setEntries([...fileSuite.suites, ...fileSuite.tests]);
const files = projectFiles(project);
const newProjectSuite = newProjectSuites.find(e => e.project()!.name === projectName);
const filesToClear = new Set(requestedFiles);
for (const fileSuite of newProjectSuite?.suites || []) {
filesToClear.delete(fileSuite.location!.file);
files.set(fileSuite.location!.file, fileSuite);
}
// We requested update for those, but got no entries.
for (const file of filesToDelete) {
const testFile = project.files.get(file);
if (testFile)
testFile.setEntries([]);
for (const file of filesToClear) {
const fileSuite = files.get(file);
if (fileSuite) {
fileSuite.suites = [];
fileSuite.tests = [];
}
}
project.suite.suites = [...files.values()];
}
this._didUpdate.fire();
}
Expand All @@ -255,15 +205,15 @@ export class TestModel {

private _updateFromRunningProject(project: TestProject, projectSuite: reporterTypes.Suite) {
// When running tests, don't remove existing entries.
const files = projectFiles(project);
for (const fileSuite of projectSuite.suites) {
if (!fileSuite.allTests().length)
continue;
let file = project.files.get(fileSuite.location!.file);
if (!file)
file = this._createFile(project, fileSuite.location!.file);
if (!file.entries())
file.setEntries([...fileSuite.suites, ...fileSuite.tests]);
const existingFileSuite = files.get(fileSuite.location!.file);
if (!existingFileSuite || !existingFileSuite.allTests().length)
files.set(fileSuite.location!.file, fileSuite);
}
project.suite.suites = [...files.values()];
this._didUpdate.fire();
}

Expand All @@ -274,29 +224,40 @@ export class TestModel {

async debugTests(projects: TestProject[], locations: string[] | null, reporter: reporterTypes.ReporterV2, parametrizedTestTitle: string | undefined, token: vscodeTypes.CancellationToken) {
locations = locations || [];
await this._playwrightTest.debugTests(this._vscode, this.config, projects.map(p => p.name), projects.map(p => p.testDir), this._envProvider(), locations, reporter, parametrizedTestTitle, token);
const testDirs = projects.map(p => p.project.testDir);
await this._playwrightTest.debugTests(this._vscode, this.config, projects.map(p => p.name), testDirs, this._envProvider(), locations, reporter, parametrizedTestTitle, token);
}

private _mapFilesToSources(files: Set<string>): Set<string> {
private _mapFilesToSources(testDirs: string[], files: Set<string>): string[] {
const result = new Set<string>();
for (const file of files) {
if (!testDirs.some(t => file.startsWith(t + '/')))
continue;
const sources = this._fileToSources.get(file);
if (sources)
sources.forEach(f => result.add(f));
else
result.add(file);
}
return result;
return [...result];
}

narrowDownFilesToEnabledProjects(fileNames: Set<string>) {
const result = new Set<string>();
for (const project of this.enabledProjects()) {
const files = projectFiles(project);
for (const fileName of fileNames) {
if (project.files.has(fileName))
if (files.has(fileName))
result.add(fileName);
}
}
return result;
}
}

export function projectFiles(project: TestProject): Map<string, reporterTypes.Suite> {
const files = new Map<string, reporterTypes.Suite>();
for (const fileSuite of project.suite.suites)
files.set(fileSuite.location!.file, fileSuite);
return files;
}
Loading

0 comments on commit 6e4cdf0

Please sign in to comment.