Files
meteor/tools/tests/package-tests.js
2023-04-25 14:18:54 -03:00

1436 lines
42 KiB
JavaScript

var _ = require("underscore");
var selftest = require("../tool-testing/selftest.js");
var Sandbox = selftest.Sandbox;
var files = require("../fs/files");
var utils = require("../utils/utils.js");
var username = "test";
// Returns a random package name.
var randomizedPackageName = function (username, start) {
// We often use package names in long, wrapped string output, so having them
// be a consistent length is very useful.
var startStr = start ? start + "-" : "";
return username + ":" + startStr + utils.randomToken().substring(0, 6);
};
// Given a sandbox, that has the app as its currend cwd, read the packages file
// and check that it contains exactly the packages specified, in order.
//
// sand: a sandbox, that has the main app directory as its cwd.
// packages: an array of packages in order. Packages can be of the form:
//
// meteor-base (ie: name), in which case this will match any
// version of that package as long as it is included.
//
// awesome-pack@1.0.0 (ie: name@version) to match that name at that
// version explicitly. This is for packages that we included at a specific
// version.
var checkPackages = selftest.markStack(async function (sand, packages) {
var lines = sand.read(".meteor/packages").split("\n");
var i = 0;
for (const line of lines) {
if (!line) {
return;
}
// If the specified package contains an @ sign, then it has a version
// number, so we should match everything.
if (packages[i].split("@").length > 1) {
await selftest.expectEqual(line, packages[i]);
} else {
var pack = line.split("@")[0];
await selftest.expectEqual(pack, packages[i]);
}
i++;
}
await selftest.expectEqual(packages.length, i);
});
// Given a sandbox, that has the app as its currend cwd, read the versions file
// and check that it contains the packages that we are looking for. We don't
// check the order, we just want to make sure that the right dependencies are
// in.
//
// sand: a sandbox, that has the main app directory as its cwd.
// packages: an array of packages in order. Packages can be of the form:
//
// meteor-base (ie: name), in which case this will match any
// version of that package as long as it is included. This is for packages
// external to the app, since we don't want this test to fail when we push a
// new version.
//
// awesome-pack@1.0.0 (ie: name@version) to match that name at that
// version explicitly. This is for packages that only exist for the purpose
// of this test (for example, packages local to this app), so we know exactly
// what version we expect.
var checkVersions = selftest.markStack(async function (sand, packages) {
var lines = sand.read(".meteor/versions").split("\n");
var depend = {};
_.each(lines, function (line) {
if (!line) {
return;
}
// Packages are stored of the form foo@1.0.0, so this should give us an
// array [foo, 1.0.0].
var split = line.split("@");
var pack = split[0];
depend[pack] = split[1];
});
var i = 0;
for (const pack of packages) {
var split = pack.split("@");
if (split.length > 1) {
await selftest.expectEqual(depend[split[0]], split[1]);
} else {
var exists = _.has(depend, split[0]);
await selftest.expectEqual(exists, true);
}
i++;
}
await selftest.expectEqual(packages.length, i);
});
// Add packages to an app. Change the contents of the packages and their
// dependencies, make sure that the app still refreshes.
selftest.define("change packages during hot code push", [], async function () {
var s = new Sandbox();
await s.init();
var run;
// Starting a run
await s.createApp("myapp", "package-tests");
s.cd("myapp");
run = s.run();
run.waitSecs(5);
await run.match("myapp");
await run.match("proxy");
run.waitSecs(5);
await run.match("your app");
run.waitSecs(5);
await run.match("running at");
await run.match("localhost");
// Add the local package 'say-something'. It should print a message.
s.write(".meteor/packages", "meteor-base \n say-something");
run.waitSecs(3);
await run.match("initial");
// Modify the local package 'say-something'.
s.cd("packages/say-something", function () {
s.write("foo.js", 'console.log("another");');
});
run.waitSecs(12);
await run.match("another");
// Add a local package depends-on-plugin.
s.write(".meteor/packages", "meteor-base \n depends-on-plugin");
run.waitSecs(2);
await run.match("foobar");
// Change something in the plugin.
s.cd("packages/contains-plugin/plugin", function () {
s.write("plugin.js", 'console.log("edit");');
});
run.waitSecs(2);
await run.match("edit");
await run.match("foobar!");
// Check that we are watching the versions file, as well as the packages file.
s.unlink(".meteor/versions");
run.waitSecs(10);
await run.match("restarted");
// Switch back to say-something for a moment.
s.write(".meteor/packages", "meteor-base \n say-something");
run.waitSecs(3);
await run.match("another");
await run.stop();
s.rename("packages/say-something", "packages/shout-something");
s.write(".meteor/packages", "meteor-base \n shout-something");
s.cd("packages/shout-something", function () {
s.write("foo.js", 'console.log("louder");');
});
run = s.run();
run.waitSecs(5);
await run.match("myapp");
await run.match("proxy");
run.waitSecs(5);
await run.match("louder"); // the package actually loaded
// How about breaking and fixing a package.js?
await s.cd("packages/shout-something", async function () {
var packageJs = s.read("package.js");
s.write("package.js", "]");
run.waitSecs(3);
await run.match("=> Errors prevented startup");
await run.match("package.js:1: Unexpected token");
await run.match("Waiting for file change");
s.write("package.js", packageJs);
run.waitSecs(3);
await run.match("restarting");
await run.match("restarted");
});
await run.stop();
});
selftest.define("add debugOnly and prodOnly packages", [], async function () {
var s = new Sandbox();
await s.init();
var run;
// Starting a run
await s.createApp("myapp", "package-tests");
s.cd("myapp");
s.set("METEOR_OFFLINE_CATALOG", "t");
// Add a debugOnly package. It should work during a normal run, but print
// nothing in production mode.
run = s.run("add", "debug-only");
run.waitSecs(30);
await run.match("debug-only");
await run.expectExit(0);
function onStartup(property) {
s.mkdir("server");
s.write(
"server/exit-test.js",
[
"Meteor.startup(() => {",
" console.log('Meteor.isDevelopment', Meteor.isDevelopment);",
" console.log('Meteor.isProduction', Meteor.isProduction);",
` console.log('${property}', global.${property});`,
` process.exit(global.${property} ? 234 : 235);`,
"});",
"",
].join("\n")
);
}
onStartup("DEBUG_ONLY_LOADED");
run = s.run("--once");
run.waitSecs(30);
await run.expectExit(234);
run = s.run("--once", "--production");
run.waitSecs(30);
await run.expectExit(235);
// Add prod-only package, which sets GLOBAL.PROD_ONLY_LOADED.
run = s.run("add", "prod-only");
await run.match("prod-only");
await run.expectExit(0);
onStartup("PROD_ONLY_LOADED");
run = s.run("--once");
run.waitSecs(30);
await run.expectExit(235);
run = s.run("--once", "--production");
run.waitSecs(30);
await run.expectExit(234);
});
// Add packages through the command line. Make sure that the correct set of
// changes is reflected in .meteor/packages, .meteor/versions and list.
selftest.define("add packages to app", [], async function () {
var s = new Sandbox();
await s.init();
var run;
// Starting a run
await s.createApp("myapp", "package-tests");
s.cd("myapp");
s.set("METEOR_OFFLINE_CATALOG", "t");
// This has legit version syntax, but accounts-base started with 1.0.0 and is
// unlikely to backtrack.
run = s.run("add", "accounts-base@0.123.123");
await run.matchErr("no such version");
await run.expectExit(1);
// Adding a nonexistent package at a nonexistent version should print
// only one error message, not two. (We used to print "no such
// package" and "no such version".)
run = s.run("add", "not-a-real-package-and-never-will-be@1.0.0");
await run.matchErr("no such package");
await run.expectExit(1);
run.forbidAll("no such version");
run = s.run("add", "accounts-base");
await run.match("accounts-base: A user account system");
await run.expectExit(0);
await checkPackages(s, ["meteor-base", "accounts-base"]);
// Adding the nonexistent version now should still say "no such
// version". Regression test for
// https://github.com/meteor/meteor/issues/2898.
run = s.run("add", "accounts-base@0.123.123");
await run.matchErr("no such version");
await run.expectExit(1);
run.forbidAll("Currently using accounts-base");
run.forbidAll("will be changed to");
run = s.run("--once");
run = s.run("add", "say-something@1.0.0");
await run.match("say-something: print to console");
await run.expectExit(0);
await checkPackages(s, [
"meteor-base",
"accounts-base",
"say-something@1.0.0",
]);
run = s.run("add", "depends-on-plugin");
await run.match(/depends-on-plugin.*added,/);
await run.expectExit(0);
await checkPackages(s, [
"meteor-base",
"accounts-base",
"say-something@1.0.0",
"depends-on-plugin",
]);
await checkVersions(s, [
"accounts-base",
"depends-on-plugin",
"say-something",
"meteor-base",
"contains-plugin@1.1.0",
]);
run = s.run("remove", "say-something");
await run.match("say-something: removed dependency");
await checkVersions(s, [
"accounts-base",
"depends-on-plugin",
"meteor-base",
"contains-plugin",
]);
run = s.run("remove", "depends-on-plugin");
await run.match(/contains-plugin.*removed from your project/);
await run.match(/depends-on-plugin.*removed from your project/);
await run.match("depends-on-plugin: removed dependency");
await checkVersions(s, ["accounts-base", "meteor-base"]);
run = s.run("list");
await run.match("accounts-base");
await run.match("meteor-base");
// Add a description-less package. Check that no weird things get
// printed (like "added no-description: undefined").
run = s.run("add", "no-description");
await run.match("no-description\n");
await run.expectEnd();
await run.expectExit(0);
});
selftest.define(
"add package with both debugOnly and prodOnly",
[],
async function () {
var s = new Sandbox();
await s.init();
var run;
// Add an app with a package with prodOnly and debugOnly set (an error)
await s.createApp("myapp", "debug-only-test", { dontPrepareApp: true });
s.cd("myapp");
run = s.run("--prepare-app");
run.waitSecs(20);
await run.matchErr(
"can't have more than one of: debugOnly, prodOnly, testOnly"
);
await run.expectExit(1);
}
);
// Add a package that adds files to specific client architectures.
selftest.define("add packages client archs", async function (options) {
var runTestWithArgs = async function (clientType, args, port) {
var s = new Sandbox({
clients: Object.assign(options.clients, { port: port }),
});
await s.init();
// Starting a run
await s.createApp("myapp", "package-tests");
s.cd("myapp");
s.set("METEOR_OFFLINE_CATALOG", "t");
var outerRun = s.run("add", "say-something-client-targets");
await outerRun.match(/say-something-client-targets.*added,/);
await outerRun.expectExit(0);
await checkPackages(s, ["meteor-base", "say-something-client-targets"]);
await s.testWithAllClients(
async function (run) {
var expectedLogNum = 0;
run.waitSecs(10);
await run.match("myapp");
await run.match("proxy");
run.waitSecs(10);
await run.match("running at");
await run.match("localhost");
run.connectClient();
run.waitSecs(40);
await run.match("all clients " + expectedLogNum++);
await run.match(clientType + " client " + expectedLogNum++);
await run.stop();
},
{
args,
testName: "add packages client archs",
testFile: "package-tests.js",
}
);
};
await runTestWithArgs("browser", [], 3000);
});
selftest.define("add package with no builds", ["net"], async function () {
var s = new Sandbox();
await s.init();
// This depends on glasser:binary-package-with-no-builds@1.0.0 existing with
// no published builds.
await s.createApp("myapp", "empty");
s.cd("myapp");
var run = s.run("add", "glasser:binary-package-with-no-builds");
run.waitSecs(10);
await run.matchErr("glasser:binary-package-with-no-builds@1.0.0");
await run.matchErr("No compatible binary build found");
await run.expectExit(1);
});
selftest.define(
"package skeleton creates correct versionsFrom",
["custom-warehouse"],
async function () {
var s = new Sandbox({ warehouse: { v1: { recommended: true } } });
await s.init();
var token = utils.randomToken();
var fullPackageName = "test:" + token;
var fsPackageName = token;
var run = s.run("create", "--package", fullPackageName);
run.waitSecs(15);
await run.match(fullPackageName);
await run.expectExit(0);
s.cd(fsPackageName);
var packageJs = s.read("package.js");
if (!packageJs.match(/api.versionsFrom\('v1'\);/)) {
selftest.fail(
"package.js missing correct 'api.versionsFrom':\n" + packageJs
);
}
}
);
selftest.define("show unknown version of package", async function () {
var s = new Sandbox();
await s.init();
// This version doesn't exist and is unlikely to exist.
var run = s.run("show", "meteor-base@0.123.456");
run.waitSecs(5);
await run.matchErr("meteor-base@0.123.456: not found");
await run.expectExit(1);
});
selftest.define("circular dependency errors", async function () {
var s = new Sandbox();
await s.init();
// meteor add refreshes, but we don't need anything from the official catalog
// here.
s.set("METEOR_OFFLINE_CATALOG", "t");
var run;
// This app contains some pairs of packages with circular dependencies The app
// currently *uses* no packages, so it can be created successfully.
await s.createApp("myapp", "circular-deps");
s.cd("myapp");
// Try to add one of a pair of circularly-depending packages. See an error.
run = s.run("add", "first");
await run.matchErr("error: circular dependency");
await run.expectExit(1);
// Note that the app still builds fine because 'first' didn't actually get
// added.
run = s.run("--prepare-app");
await run.expectExit(0);
// This pair has first-imply uses second-imply, second-imply implies
// first-imply.
run = s.run("add", "first-imply");
await run.matchErr("error: circular dependency");
await run.expectExit(1);
// This pair has first-weak uses second-weak, second-weak uses first-weak
// weakly. Currently, it's possible to add a weak cycle to an app (ie, the
// prepare-app step passes), but not to run the bundler. We don't want to
// write a test that prevents us from making the weak cycle an error at
// prepare-time, so let's skip straight to bundling.
s.write(".meteor/packages", "first-weak");
run = s.run("--once");
await run.matchErr("error: circular dependency");
await run.expectExit(254);
// ... but we can add second-weak, which just doesn't pull in first-weak at
// all.
s.write(".meteor/packages", "second-weak");
run = s.run("--once");
await run.match(/first-weak.*removed from your project/);
await run.expectExit(123); // the app immediately calls process.exit(123)
// This pair has first-unordered uses second-unordered, second-unordered uses
// first-unordered unorderedly. This should work just fine: that's why
// unordered exists!
s.write(".meteor/packages", "first-unordered");
run = s.run("--once");
await run.match(/first-unordered.*added/);
await run.match(/second-unordered.*added/);
await run.match(/second-weak.*removed from your project/);
await run.expectExit(123); // the app immediately calls process.exit(123)
});
// Runs 'meteor show <fullPackageName>' without a specified version and checks
// that the output is correct.
//
// - s: sandbox in which to run commands
// - fullPackageName: name of the package to show.
// - options:
// - summary: Expected summary of the latest version.
// - description: longform description of the latest version
// - maintainers: the string of maintainers
// - homepage: Homepage url, if one was set.
// - git: Git url, if one was set.
// - exports: exports string
// - implies: implies string
// - defaultVersion: version that git/exports/etc come from
// - versions: array of objects representing versions that we have
// published, with keys:
// - version: version number (ex: 0.9.9)
// - date: string we expect to see as the date.
// - label: string that we expect to see as the label. (ex: "installed")
// - addendum: a message to display at the bottom.
// - all: run 'meteor show' with the 'show-all' option.
var testShowPackage = selftest.markStack(async function (
s,
fullPackageName,
options
) {
var run;
if (options.all) {
run = s.run("show", "--show-all", fullPackageName);
} else {
run = s.run("show", fullPackageName);
}
var packageName = options.defaultVersion
? fullPackageName + "@" + options.defaultVersion
: fullPackageName;
await run.match("Package: " + packageName + "\n");
if (options.homepage) {
await run.read("Homepage: " + options.homepage + "\n");
}
if (options.maintainers) {
await run.read("Maintainers: " + options.maintainers + "\n");
}
if (options.git) {
await run.read("Git: " + options.git + "\n");
}
if (options.exports) {
await run.read("Exports: " + options.exports + "\n");
}
if (options.implies) {
await run.read("Implies: " + options.implies + "\n");
}
await run.read("\n");
if (_.has(options, "description")) {
await run.read(options.description + "\n");
} else if (_.has(options, "summary")) {
await run.read(options.summary + "\n");
}
if (options.versions) {
if (options.all) {
await run.match("Versions:");
} else {
await run.match("Recent versions:");
}
for (const version of options.versions) {
await run.match(version.version);
if (version.directory) {
await run.match(version.directory + "\n");
} else {
await run.match(version.date);
if (version.label) {
await run.match(version.label + "\n");
} else {
await run.match("\n");
}
}
}
await run.read("\n");
}
if (options.addendum) {
await run.read(options.addendum);
}
await run.expectExit(0);
});
// Runs 'meteor show <name>@<version> and checks that the output is correct.
//
// - s: sandbox
// - options:
// - packageName: name of the package.
// - version: version string.
// - summary: summary string of the package.
// - description: long-form description of the package
// - publishedBy: username of the publisher.
// - publishedOn: string of the publication time.
// - git: (optional) URL of the git repository.
// - dependencies: (optional) an array of objects representing dependencies:
// - name: package name
// - constraint: constraint, such as "1.0.0" or "=1.0.0" or null.
// - weak: true if this is a weak dependency.
// - addendum: a message that we expect to display at the very bottom.
var testShowPackageVersion = selftest.markStack(async function (s, options) {
var name = options.packageName;
var version = options.version;
var run = s.run("show", name + "@" + version);
await run.match("Package: " + name + "@" + version + "\n");
if (options.directory) {
await run.match("Directory: " + options.directory + "\n");
}
if (options.exports) {
await run.read("Exports: " + options.exports + "\n");
}
if (options.implies) {
await run.read("Implies: " + options.implies + "\n");
}
if (options.git) {
await run.match("Git: " + options.git + "\n");
}
if (_.has(options, "description")) {
await run.read("\n");
await run.read(options.description + "\n");
} else if (_.has(options, "summary")) {
await run.read("\n");
await run.read(options.summary + "\n");
}
if (options.dependencies) {
await run.read("\n");
await run.read("Depends on:\n");
// Use 'read' to ensure that these are the only dependencies listed.
_.each(options.dependencies, async function (dep) {
var depStr = dep.name;
if (dep.constraint) {
depStr += "@" + dep.constraint;
}
if (dep.weak) {
depStr += " (weak dependency)";
}
await run.read(" " + depStr + "\n");
});
}
if (options.publishedBy) {
await run.match("\n");
await run.match(
"Published by " +
options.publishedBy +
" on " +
options.publishedOn +
".\n"
);
}
if (options.addendum) {
await run.read("\n" + options.addendum + "\n");
}
// Make sure that we exit without printing anything else.
await run.expectEnd(0);
});
// For local packages without a version, we want to replace version information
// with the string "local". We also want to make sure that querying for
// 'name@local' gives that local version.
selftest.define("show local package w/o version", async function () {
var s = new Sandbox();
await s.init();
var name = "my-local-package" + utils.randomToken();
// Create a package without version or summary; check that we can show its
// information without crashing.
await s.createPackage(name, name, "package-for-show");
var packageDir = files.pathJoin(s.root, "home", name);
await s.cd(name, async function () {
s.cp("package-completely-empty.js", "package.js");
await testShowPackage(s, name, {
defaultVersion: "local",
versions: [{ version: "local", directory: packageDir }],
});
await testShowPackageVersion(s, {
packageName: name,
version: "local",
directory: packageDir,
});
// Test that running without any arguments also shows this package.
var run = s.run("show");
await run.match("Package: " + name + "@local\n");
await run.match("Directory: " + packageDir + "\n");
await run.expectExit(0);
});
// Test that running without any arguments outside of a package does not
// work.
var run = s.run("show");
await run.matchErr("specify a package or release name");
await run.expectExit(1);
});
// Make sure that a local-only package shows up correctly in show and search
// results.
selftest.define("show and search local package", async function () {
// Setup: create an app, containing a package. This local package should show
// up in the results of `meteor show` and `meteor search`.
var s = new Sandbox();
await s.init();
var name = "my-local-package" + utils.randomToken();
await s.createApp("myapp", "empty");
s.cd("myapp");
s.mkdir("packages");
await s.cd("packages", function () {
return s.createPackage(name, name, "package-for-show");
});
var packageDir = files.pathJoin(s.root, "home", "myapp", "packages", name);
s.cd(packageDir, function () {
s.cp("package-with-git.js", "package.js");
});
var summary = "This is a test package";
// Run `meteor show`, but don't add the package to the app yet. We should know
// that the package exists, even though it hasn't been added to the app.
await testShowPackage(s, name, {
summary: summary,
defaultVersion: "local",
git: "www.github.com/meteor/meteor",
versions: [{ version: "1.0.0", directory: packageDir }],
});
// Add the package to the app.
var run = s.run("add", name);
run.waitSecs(5);
await run.expectExit(0);
await testShowPackage(s, name, {
summary: summary,
git: "www.github.com/meteor/meteor",
defaultVersion: "local",
versions: [{ version: "1.0.0", directory: packageDir }],
});
// When we run `meteor search`, we should be able to see the results for this
// package, even though it does not exist on the server.
run = s.run("search", name);
run.waitSecs(15);
await run.match(name);
await run.match("You can use");
await run.expectExit(0);
// We can see exports on local packages.
s.cd("packages");
summary = "This is a test package";
name = "my-local-exports";
packageDir = files.pathJoin(s.root, "home", "myapp", "packages", name);
await s.createPackage(name, name, "package-for-show");
s.cd(name, function () {
s.cp("package-with-exports.js", "package.js");
});
const impRaw = {
A: "",
B: "server",
C: "web.browser, web.browser.legacy, web.cordova",
D: "web.browser, web.browser.legacy",
E: "web.cordova",
G: "server, web.cordova",
};
const exportStr = Object.keys(impRaw)
.map((key) => {
const value = impRaw[key];
return key + (value ? " (" + value + ")" : "");
})
.join(", ");
var description = "Test package.";
await testShowPackage(s, name, {
summary: summary,
git: "www.github.com/meteor/meteor",
exports: exportStr,
description: description,
defaultVersion: "local",
versions: [{ version: "1.0.1", directory: packageDir }],
});
await testShowPackageVersion(s, {
packageName: name,
version: "1.0.1",
directory: packageDir,
git: "www.github.com/meteor/meteor",
summary: summary,
exports: exportStr,
description: description,
});
// Test showing implies. Since we are not going to build the package, we don't
// have to publish any of the things that we imply.
var impliesData = _.sortBy(
_.map(impRaw, function (label, placeholder) {
var name = randomizedPackageName(username, placeholder.toLowerCase());
return { placeholder: placeholder, name: name, label: label };
}),
"name"
);
s.cd(name, function () {
s.cp("package-with-implies.js", "package.js");
var packOpen = s.read("package.js");
_.each(impliesData, function (d) {
var repReg = new RegExp("~" + d.placeholder + "~", "g");
packOpen = packOpen.replace(repReg, d.name);
});
s.write("package.js", packOpen);
});
summary = "This is a test package";
description = "Test package.";
var impArr = _.map(impliesData, function (d) {
return d.label ? d.name + " (" + d.label + ")" : d.name;
});
var impStr =
impArr[0] +
", " +
impArr[1] +
", " +
impArr[2] +
", " +
impArr[3] +
", " +
impArr[4] +
", " +
impArr[5];
await testShowPackage(s, name, {
summary: summary,
description: description,
implies: impStr,
directory: packageDir,
defaultVersion: "local",
git: "www.github.com/meteor/meteor",
versions: [{ version: "1.2.1", directory: packageDir }],
});
// Implies are also dependencies.
var deps = _.map(impliesData, function (d) {
return { name: d.name, constraint: "1.0.0" };
});
await testShowPackageVersion(s, {
packageName: name,
version: "1.2.1",
directory: packageDir,
description: description,
summary: summary,
git: "www.github.com/meteor/meteor",
implies: impStr,
dependencies: deps,
});
});
// This tests that we get the right excerpt out of the Readme.md in different
// combinations. It doesn't test publication, because publishing is slow --
// that's covered in a different test.
selftest.define("show readme excerpt", async function () {
var s = new Sandbox();
await s.init();
var name = "my-local-package" + utils.randomToken();
// Create a package without version or summary; check that we can show its
// information without crashing.
await s.createPackage(name, name, "package-for-show");
var packageDir = files.pathJoin(s.root, "home", name);
// We are just going to change the description in the Readme. Some things
// about this package are not going to change, and our test will be more
// legible to factor them out here.
var basePackageInfo = {
summary: "This is a test package",
defaultVersion: "local",
versions: [{ version: "0.9.9", directory: packageDir }],
};
var baseVersionInfo = {
summary: "This is a test package",
packageName: name,
version: "0.9.9",
directory: packageDir,
};
s.cd(name);
// By default, we will use the README.md file for documentation.
// Start with a blank file. Nothing should show up!
s.write("README.md", "");
await testShowPackage(s, name, basePackageInfo);
await testShowPackageVersion(s, baseVersionInfo);
// An example of a standard readme.
var readme =
"Heading" +
"\n" +
"========" +
"\n" +
"foobar1" +
"\n" +
"\n" +
"## Subheading" +
"\n" +
"You should not see this line!";
s.write("README.md", readme);
await testShowPackage(
s,
name,
Object.assign({ description: "foobar1" }, basePackageInfo)
);
await testShowPackageVersion(
s,
Object.assign({ description: "foobar1" }, baseVersionInfo)
);
// Another example -- we have hidden the excerpt under a different subheading.
readme =
"Heading" +
"\n" +
"========" +
"\n" +
"## Subheading" +
"\n" +
"foobar2" +
"\n" +
"## Another subheading" +
"\n" +
"You should not see this line!";
s.write("README.md", readme);
await testShowPackage(
s,
name,
Object.assign({ description: "foobar2" }, basePackageInfo)
);
await testShowPackageVersion(
s,
Object.assign({ description: "foobar2" }, baseVersionInfo)
);
// Generally, people skip a line between the header and the text, and
// sometimes, even between headers. (It is part of markdown, in fact.) Let's
// make sure that we handle that correctly.
readme =
"Heading" +
"\n" +
"========" +
"\n" +
"\n" +
"## Subheading" +
"\n" +
"\n" +
"foobaz" +
"\n" +
"\n" +
"## Another subheading" +
"\n" +
"\n" +
"You should not see this line!";
s.write("README.md", readme);
await testShowPackage(
s,
name,
Object.assign({ description: "foobaz" }, basePackageInfo)
);
await testShowPackageVersion(
s,
Object.assign({ description: "foobaz" }, baseVersionInfo)
);
// Some formatting in the text.
var excerpt = "Here is a code sample:" + "\n\n" + "```foobar and foobar```";
readme =
"Heading" +
"\n" +
"========" +
"\n" +
"\n" +
excerpt +
"\n\n" +
"# Subheading" +
"\n" +
"\n" +
"## Another subheading" +
"\n" +
"\n" +
"You should not see this line!";
s.write("README.md", readme);
await testShowPackage(
s,
name,
Object.assign({ description: excerpt }, basePackageInfo)
);
await testShowPackageVersion(
s,
Object.assign({ description: excerpt }, baseVersionInfo)
);
// Now, let's try different file specifications for the documentation.
var git = "https:ilovegit.git";
var summary = "Test summary";
var staging = s.read("package-customizable.js");
staging = staging.replace(/~version~/g, "1.0.0");
staging = staging.replace(/~git~/g, git);
staging = staging.replace(/~summary~/g, summary);
// If we specify null, we have no docs.
s.write("package.js", staging.replace(/~documentation~/g, "null"));
baseVersionInfo = {
summary: summary,
git: git,
packageName: name,
version: "1.0.0",
directory: packageDir,
};
basePackageInfo = {
git: git,
summary: summary,
defaultVersion: "local",
versions: [{ version: "1.0.0", directory: packageDir }],
};
await testShowPackageVersion(s, baseVersionInfo);
await testShowPackage(s, name, basePackageInfo);
// If we specify a different file, read that file.
s.write(
"package.js",
staging.replace(/~documentation~/g, "'Meteor-Readme.md'")
);
readme = "A special Readme, just for Meteor.";
s.write("Meteor-Readme.md", "Title\n==\n" + readme);
await testShowPackageVersion(
s,
Object.assign({ description: readme }, baseVersionInfo)
);
await testShowPackage(
s,
name,
Object.assign({ description: readme }, basePackageInfo)
);
// If we specify a non-existent file, tell us.
s.write("package.js", staging.replace(/~documentation~/g, "'NOTHING'"));
var run = s.run("show", name);
await run.matchErr("Documentation not found");
await run.expectExit(1);
run = s.run("show", name + "@1.0.0");
await run.matchErr("Documentation not found");
await run.expectExit(1);
});
selftest.define("tilde version constraints", [], async function () {
var s = new Sandbox();
await s.init();
s.set("METEOR_WATCH_PRIORITIZE_CHANGED", "false");
await s.createApp("tilde-app", "package-tests");
s.cd("tilde-app");
var run = s.run();
await run.match("tilde-app");
await run.match("proxy");
run.waitSecs(10);
await run.match("your app");
run.waitSecs(10);
await run.match("running at");
run.waitSecs(60);
var packages = s.read(".meteor/packages").replace(/\n*$/m, "\n");
function setTopLevelConstraint(constraint) {
s.write(
".meteor/packages",
packages +
"tilde-constraints" +
(constraint ? "@" + constraint : "") +
"\n"
);
}
setTopLevelConstraint("");
await run.match(/tilde-constraints.*added, version 0\.4\.2/);
await run.match("tilde-constraints.js");
run.waitSecs(10);
setTopLevelConstraint("0.4.0");
await run.match("tilde-constraints.js");
await run.match("server restarted");
run.waitSecs(10);
setTopLevelConstraint("~0.4.0");
await run.match("tilde-constraints.js");
await run.match("server restarted");
run.waitSecs(10);
setTopLevelConstraint("0.4.3");
await run.match(
"error: No version of tilde-constraints satisfies all constraints"
);
run.waitSecs(10);
setTopLevelConstraint("~0.4.3");
await run.match(
"error: No version of tilde-constraints satisfies all constraints"
);
run.waitSecs(10);
setTopLevelConstraint("0.3.0");
await run.match("tilde-constraints.js");
await run.match("server restarted");
run.waitSecs(10);
setTopLevelConstraint("~0.3.0");
await run.match(
"error: No version of tilde-constraints satisfies all constraints"
);
run.waitSecs(10);
setTopLevelConstraint("0.5.0");
await run.match(
"error: No version of tilde-constraints satisfies all constraints"
);
run.waitSecs(10);
setTopLevelConstraint("~0.5.0");
await run.match(
"error: No version of tilde-constraints satisfies all constraints"
);
run.waitSecs(10);
s.write(".meteor/packages", packages);
await run.match(/tilde-constraints.*removed/);
run.waitSecs(10);
s.write(".meteor/packages", packages + "tilde-dependent\n");
await run.match(/tilde-constraints.*added, version 0\.4\.2/);
await run.match(/tilde-dependent.*added, version 0\.1\.0/);
await run.match("tilde-constraints.js");
await run.match("tilde-dependent.js");
run.waitSecs(10);
var depPackageJsPath = "packages/tilde-dependent/package.js";
var depPackageJs = s.read(depPackageJsPath);
function setDepConstraint(constraint) {
s.write(
depPackageJsPath,
depPackageJs.replace(
/tilde-constraints[^"]*/g, // Syntax highlighting hack: "
"tilde-constraints" + (constraint ? "@" + constraint : "")
)
);
}
setDepConstraint("0.4.0");
await run.match("tilde-constraints.js");
await run.match("tilde-dependent.js");
await run.match("server restarted");
run.waitSecs(10);
setDepConstraint("~0.4.0");
await run.match("tilde-constraints.js");
await run.match("tilde-dependent.js");
await run.match("server restarted");
run.waitSecs(10);
setDepConstraint("0.3.0");
await run.match("tilde-constraints.js");
await run.match("tilde-dependent.js");
await run.match("server restarted");
run.waitSecs(10);
// TODO The rest of these tests should cause version conflicts, but it
// seems like version constraints between local packages are ignored,
// which is a larger (preexisting) problem we should investigate.
/*
setDepConstraint("=0.4.0");
await run.match("error: No version of tilde-constraints satisfies all constraints");
run.waitSecs(10);
setDepConstraint("~0.3.0");
await run.match("error: No version of tilde-constraints satisfies all constraints");
run.waitSecs(10);
setDepConstraint("0.4.3");
await run.match("error: No version of tilde-constraints satisfies all constraints");
run.waitSecs(10);
setDepConstraint("~0.4.3");
await run.match("error: No version of tilde-constraints satisfies all constraints");
run.waitSecs(10);
*/
await run.stop();
});
selftest.define("override version constraints", [], async function () {
var s = new Sandbox();
await s.init();
// The constraint solver avoids re-solving everything from scratch on
// rebuilds if the current input of top-level constraints matches the
// previously solved input (also just top-level constraints). This is
// slightly unsound, because non-top-level dependency constraints might
// have changed, but it's important for performance, and relatively
// harmless in practice (if there's a version conflict, you'll find out
// about it the next time you do a full restart of the development
// server). The unsoundness causes problems for this test, however, and
// since we're not testing the caching functionality here, we set this
// environment variable to disable the caching completely.
s.set("METEOR_DISABLE_CONSTRAINT_SOLVER_CACHING", "true");
await s.createApp("override-app", "package-tests");
s.cd("override-app");
var run = s.run();
await run.match("override-app");
await run.match("proxy");
run.waitSecs(10);
await run.match("your app");
run.waitSecs(10);
await run.match("running at");
run.waitSecs(60);
let packages = s.read(".meteor/packages").replace(/\n*$/m, "\n");
function setTopLevelConstraint(constraint) {
s.write(
".meteor/packages",
packages +
"override-constraints" +
(constraint ? "@" + constraint : "") +
"\n"
);
}
async function checkRestarted() {
await run.match("override-constraints.js");
await run.match("server restarted");
run.waitSecs(10);
}
setTopLevelConstraint("");
await run.match(/override-constraints.*added, version 1\.5\.3/);
await checkRestarted();
setTopLevelConstraint("1.4.0");
await checkRestarted();
setTopLevelConstraint("1.4.0!");
await checkRestarted();
setTopLevelConstraint("=1.5.3");
await checkRestarted();
setTopLevelConstraint("=1.5.3!");
await checkRestarted();
async function checkNoSatisfyingVersion() {
await run.match(
"error: No version of override-constraints satisfies all constraints"
);
run.waitSecs(10);
}
setTopLevelConstraint("~1.4.0");
await checkNoSatisfyingVersion();
setTopLevelConstraint("~1.4.0!");
await checkNoSatisfyingVersion();
setTopLevelConstraint("1.6.0");
await checkNoSatisfyingVersion();
setTopLevelConstraint("1.6.0!");
await checkNoSatisfyingVersion();
setTopLevelConstraint("1.5.4");
await checkNoSatisfyingVersion();
setTopLevelConstraint("1.5.4!");
await checkNoSatisfyingVersion();
setTopLevelConstraint("~1.4.0||=1.5.3");
await checkRestarted();
setTopLevelConstraint("~1.4.0||=1.5.3!");
await checkRestarted();
// The ! applies to the whole || disjunction, not just the rightmost
// individual constraint (~1.4.0). This would fail if ~1.4.0 won.
setTopLevelConstraint("1.5.0||~1.4.0!");
await checkRestarted();
// Different major versions, but at least one of them works.
setTopLevelConstraint("1.5.0||0.4.0!");
await checkRestarted();
async function checkInvalidConstraint() {
await run.match(".meteor/packages: Invalid constraint string");
run.waitSecs(10);
}
setTopLevelConstraint("!");
await checkInvalidConstraint();
// Reset to something that works in between invalid tests.
setTopLevelConstraint("1.5.3");
await checkRestarted();
async function checkInvalidSemver() {
await run.match(".meteor/packages: Version string must look like semver");
run.waitSecs(10);
}
setTopLevelConstraint("5!");
await checkInvalidSemver();
// Reset to something that works in between invalid tests.
setTopLevelConstraint("1.5.3");
await checkRestarted();
setTopLevelConstraint("1.5!");
await checkInvalidSemver();
// Add the conflicting package.
packages += "override-conflicts@1.0.0\n";
setTopLevelConstraint("1.5.3");
await run.match(/override-conflicts.*added, version 1\.0\.1/);
await run.match("override-conflicts.js");
await checkRestarted();
const conflictingPackageJs = s.read("packages/override-conflicts/package.js");
function setConflictingConstraint(statement) {
s.write(
"packages/override-conflicts/package.js",
conflictingPackageJs.replace("// PLACEHOLDER", statement)
);
}
setConflictingConstraint('api.use("override-constraints");');
await checkRestarted();
setConflictingConstraint('api.use("override-constraints@1.5.0");');
await checkRestarted();
setConflictingConstraint('api.imply("override-constraints@1.5.0");');
await checkRestarted();
setConflictingConstraint('api.use("override-constraints@=1.4.0");');
await run.match(
"Constraint override-constraints@=1.4.0 is not satisfied by " +
"override-constraints 1.5.3"
);
run.waitSecs(10);
setTopLevelConstraint("1.5.0!");
await checkRestarted();
// Constraints imposed elsewhere are still enforced as minimums, so the
// @1.5.0! override syntax can't do anything about this constraint:
setConflictingConstraint('api.use("override-constraints@1.6.0");');
await run.match(
"Constraint override-constraints@1.6.0 is not satisfied by " +
"override-constraints 1.5.3"
);
run.waitSecs(10);
setConflictingConstraint('api.use("override-constraints@1.5.0");');
await checkRestarted();
setTopLevelConstraint("1.5.3");
await checkRestarted();
setConflictingConstraint('api.use("override-constraints@0.9.0");');
await run.match(
"Constraint override-constraints@0.9.0 is not satisfied by " +
"override-constraints 1.5.3"
);
run.waitSecs(10);
setTopLevelConstraint("0.9.0||1.5.3");
await run.match(
"Constraint override-constraints@0.9.0 is not satisfied by " +
"override-constraints 1.5.3"
);
run.waitSecs(10);
setTopLevelConstraint("1.4.0!");
await checkRestarted();
setTopLevelConstraint("~1.5.0!");
await checkRestarted();
setTopLevelConstraint("=1.5.3!");
await checkRestarted();
setTopLevelConstraint("1.5.3");
await run.match(
"Constraint override-constraints@0.9.0 is not satisfied by " +
"override-constraints 1.5.3"
);
run.waitSecs(10);
setTopLevelConstraint("");
await run.match(
"Constraint override-constraints@0.9.0 is not satisfied by " +
"override-constraints 1.5.3"
);
run.waitSecs(10);
setConflictingConstraint("// PLACEHOLDER");
await checkRestarted();
await run.stop();
});