blob: 8d83bdfdd7fae7bf963a37653c2e7ee4a45c03cd [file] [log] [blame]
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:io';
import 'package:io/io.dart';
import 'package:path/path.dart' as p;
import 'package:pubspec_parse/pubspec_parse.dart';
import 'package:test_common/utilities.dart';
enum IndexBaseMode { noBase, base }
/// Project definitions from projects defined in webdev/fixtures.
class TestProject {
final String packageName;
final String packageDirectory;
final String webAssetsPath;
final String dartEntryFileName;
final String htmlEntryFileName;
late Directory _fixturesCopy;
/// The top level directory in which we run the test server, e.g.
/// "/tmp/_test".
String get absolutePackageDirectory =>
p.join(_fixturesCopy.absolute.path, packageDirectory);
/// The directory to build and serve, e.g. "example".
String get directoryToServe => p.split(webAssetsPath).first;
/// The path to the HTML file to serve, relative to the [directoryToServe],
/// e.g. "hello_world/index.html".
String get filePathToServe {
final pathParts = p
.split(webAssetsPath)
.where((pathPart) => pathPart != directoryToServe);
return webCompatiblePath([...pathParts, htmlEntryFileName]);
}
/// The path to the Dart entry file, e.g,
/// "/tmp/_test/example/hello_world/main.dart":
String get dartEntryFilePath => p.joinAll([
_fixturesCopy.absolute.path,
packageDirectory,
webAssetsPath,
dartEntryFileName,
]);
/// The URI for the package_config.json is located in:
/// `<project directory>/.dart_tool/package_config`
Uri get packageConfigFile => p.toUri(
p.join(absolutePackageDirectory, '.dart_tool', 'package_config.json'),
);
/// The package URI of the Dart entry file, e.g,
/// "org-dartlang-app:example/hello_world/main.dart":
Uri get dartEntryFilePackageUri => Uri.parse(
'org-dartlang-app:///${p.join(webAssetsPath, dartEntryFileName)}',
);
TestProject.testPackage({IndexBaseMode baseMode = IndexBaseMode.noBase})
: this._(
packageName: '_test_package',
packageDirectory: '_test_package',
webAssetsPath: 'web',
dartEntryFileName: 'main.dart',
htmlEntryFileName:
baseMode == IndexBaseMode.base ? 'base_index.html' : 'index.html',
);
static final testCircular1 = TestProject._(
packageName: '_test_circular1',
packageDirectory: '_test_circular1',
webAssetsPath: 'web',
dartEntryFileName: 'main.dart',
htmlEntryFileName: 'index.html',
);
TestProject.testCircular2({IndexBaseMode baseMode = IndexBaseMode.noBase})
: this._(
packageName: '_test_circular2',
packageDirectory: '_test_circular2',
webAssetsPath: 'web',
dartEntryFileName: 'main.dart',
htmlEntryFileName:
baseMode == IndexBaseMode.base ? 'base_index.html' : 'index.html',
);
static final test = TestProject._(
packageName: '_test',
packageDirectory: '_test',
webAssetsPath: 'example/hello_world',
dartEntryFileName: 'main.dart',
htmlEntryFileName: 'index.html',
);
static final testScopes = TestProject._(
packageName: '_test',
packageDirectory: '_test',
webAssetsPath: webCompatiblePath(['example', 'scopes']),
dartEntryFileName: 'main.dart',
htmlEntryFileName: 'scopes.html',
);
static final testAppendBody = TestProject._(
packageName: '_test',
packageDirectory: '_test',
webAssetsPath: webCompatiblePath(['example', 'append_body']),
dartEntryFileName: 'main.dart',
htmlEntryFileName: 'index.html',
);
static final testExperiment = TestProject._(
packageName: '_experiment',
packageDirectory: '_experiment',
webAssetsPath: 'web',
dartEntryFileName: 'main.dart',
htmlEntryFileName: 'index.html',
);
static final testHotRestart1 = TestProject._(
packageName: '_test_hot_restart1',
packageDirectory: '_test_hot_restart1',
webAssetsPath: 'web',
dartEntryFileName: 'main.dart',
htmlEntryFileName: 'index.html',
);
/// This series of hot restart tests is divided across multiple packages in
/// order to test correctness when only a subset of libraries are updated.
static final testHotRestart2 = TestProject._(
packageName: '_test_hot_restart2',
packageDirectory: '_test_hot_restart2',
webAssetsPath: 'web',
dartEntryFileName: 'main.dart',
htmlEntryFileName: 'index.html',
);
static final testHotRestartBreakpoints = TestProject._(
packageName: '_test_hot_restart_breakpoints',
packageDirectory: '_test_hot_restart_breakpoints',
webAssetsPath: 'web',
dartEntryFileName: 'main.dart',
htmlEntryFileName: 'index.html',
);
static final testHotReload = TestProject._(
packageName: '_test_hot_reload',
packageDirectory: '_test_hot_reload',
webAssetsPath: 'web',
dartEntryFileName: 'main.dart',
htmlEntryFileName: 'index.html',
);
static final testHotReloadBreakpoints = TestProject._(
packageName: '_test_hot_reload_breakpoints',
packageDirectory: '_test_hot_reload_breakpoints',
webAssetsPath: 'web',
dartEntryFileName: 'main.dart',
htmlEntryFileName: 'index.html',
);
TestProject._({
required this.packageName,
required this.packageDirectory,
required this.webAssetsPath,
required this.dartEntryFileName,
required this.htmlEntryFileName,
});
static Future<void> _copyPackageAndPathDependenciesIntoTempDirectory(
Directory tempDirectory,
String packageDirectory,
Set<String> copiedPackageDirectories,
) async {
// There may be cycles in dependencies, so check that we already copied this
// package.
if (copiedPackageDirectories.contains(packageDirectory)) return;
final currentPath = absolutePath(pathFromFixtures: packageDirectory);
final newPath = p.join(tempDirectory.absolute.path, packageDirectory);
Directory(newPath).createSync();
copyPathSync(currentPath, newPath);
copiedPackageDirectories.add(packageDirectory);
final pubspec = Pubspec.parse(
File(p.join(currentPath, 'pubspec.yaml')).readAsStringSync(),
);
for (final dependency in pubspec.dependencies.values) {
if (dependency is PathDependency) {
final dependencyDirectory = Directory(
p.normalize(p.join(currentPath, dependency.path)),
);
// It may be okay to do some more complicated copying here for path
// dependencies that aren't immediately under `fixtures`, but for now,
// only support those that are.
assert(
dependencyDirectory.parent.path == Directory(currentPath).parent.path,
'Path dependency of $packageDirectory: '
'${dependencyDirectory.path} is not an immediate directory in '
'`fixtures`.',
);
await _copyPackageAndPathDependenciesIntoTempDirectory(
tempDirectory,
p.basename(dependencyDirectory.path),
copiedPackageDirectories,
);
}
}
// Clean up the project.
// Called when we need to rebuild sdk and the app from previous test
// configurations.
await Process.run('dart', [
'run',
'build_runner',
'clean',
], workingDirectory: newPath);
}
Future<void> setUp() async {
// Verify that the web assets path has no starting slash.
assert(!webAssetsPath.startsWith('/'));
// Copy the package into a temporary directory to allow editing of files if
// needed.
final systemTempDir = Directory(
// Resolve symbolic links as build_daemon tests rely on paths matching
// between the client and the daemon.
Directory.systemTemp.resolveSymbolicLinksSync(),
);
_fixturesCopy = systemTempDir.createTempSync();
await _copyPackageAndPathDependenciesIntoTempDirectory(
_fixturesCopy,
packageDirectory,
{},
);
}
/// Delete the copy of the project.
Future<void> tearDown() async {
try {
_fixturesCopy.deleteSync(recursive: true);
} on FileSystemException catch (_) {
// On Windows, the build daemon process might still be accessing the
// working directory, so try again with an exponential backoff.
var seconds = 1;
final maxAttempts = 3;
for (var attempt = 0; attempt < maxAttempts; attempt++) {
try {
_fixturesCopy.deleteSync(recursive: true);
break;
} on FileSystemException catch (_) {
await Future.delayed(Duration(seconds: seconds));
seconds *= 2;
}
}
}
}
/// The path to the Dart specified file in the 'lib' directory, e.g,
/// "/tmp/_test/lib/library.dart":
String dartLibFilePath(String dartLibFileName) => p.joinAll([
_fixturesCopy.absolute.path,
packageDirectory,
'lib',
dartLibFileName,
]);
}