diff --git a/tools/project.js b/tools/project.js index bbbfa2d3aa..3eccdb6918 100644 --- a/tools/project.js +++ b/tools/project.js @@ -330,7 +330,9 @@ _.extend(Project.prototype, { return path.join(self.rootDir, '.meteor', 'packages'); }, - // Give the contents of the project's .meteor/versions file to the caller. + // Give the contents of the project's .meteor/versions file to the + // caller, possibly after recalculating dependencies and rewriting the + // versions file. // // Returns an object mapping package name to its string version. getVersions : function () { @@ -676,3 +678,4 @@ _.extend(Project.prototype, { // cumbersome, that is our general design pattern for singletons (ex: // packageCache.packageCache, etc) project.project = new Project(); +project.Project = Project; diff --git a/tools/stats.js b/tools/stats.js index 56ccef6026..aeb19e6ad4 100644 --- a/tools/stats.js +++ b/tools/stats.js @@ -18,11 +18,32 @@ var optOutPackageName = "package-stats-opt-out"; // indirectly. Formatted as a list of objects with 'name', 'version' // and 'direct', which is how the `recordAppPackages` method on the // stats server expects to get this list. -var packageList = function () { - var directDeps = project.project.getConstraints(); +// +// In tests, we want to use the same logic to calculate the list of +// packages for an app created in a sandbox, but we don't want to run +// the constraint solver, try to load local packages from the catalog, +// etc. In particular, we don't want to have to repoint project.project +// at whatever random app we just created in a sandbox and re-initialize +// the catalog with its local packages (and then have to undo all that +// after the test is over). So tests can pass a project.Project as an +// argument, and we'll calculate the list of packages just by looking at +// .meteor/packages and .meteor/versions, not by doing anything fancy +// like running the constraint solver. +// NOTE: This means that if you pass `_currentProjectForTest`, we assume +// that it is pointing to a root directory with an existing +// .meteor/versions file. +var packageList = function (_currentProjectForTest) { + var directDeps = (_currentProjectForTest || project.project).getConstraints(); + + var versions; + if (_currentProjectForTest) { + versions = _currentProjectForTest.dependencies; + } else { + versions = project.project.getVersions(); + } return _.map( - project.project.getVersions(), + versions, function (version, name) { return { name: name, @@ -55,10 +76,10 @@ var recordPackages = function () { }; if (! release.current.isCheckout()) { - userAgentInfo.meteorReleaseTrack = release.getReleaseTrack(); - userAgentInfo.meteorReleaseVersion = release.getReleaseVersion(); + userAgentInfo.meteorReleaseTrack = release.current.getReleaseTrack(); + userAgentInfo.meteorReleaseVersion = release.current.getReleaseVersion(); userAgentInfo.meteorToolsPackageWithVersion = - release.getToolsPackageAtVersion(); + release.current.getToolsPackageAtVersion(); } try { @@ -102,10 +123,10 @@ var logErrorIfRunningMeteorRelease = function (err) { // Used in a test (and can only be used against the testing packages // server) to fetch one package stats entry for a given application. -var getPackagesForAppIdInTest = function () { +var getPackagesForAppIdInTest = function (currentProject) { return connectToPackagesStatsServer().call( "getPackagesForAppId", - project.project.getAppIdentifier()); + currentProject.getAppIdentifier()); }; var connectToPackagesStatsServer = function () { diff --git a/tools/tests/apps/package-stats-tests/.meteor/.gitignore b/tools/tests/apps/package-stats-tests/.meteor/.gitignore new file mode 100644 index 0000000000..4083037423 --- /dev/null +++ b/tools/tests/apps/package-stats-tests/.meteor/.gitignore @@ -0,0 +1 @@ +local diff --git a/tools/tests/apps/package-stats-tests/.meteor/identifier b/tools/tests/apps/package-stats-tests/.meteor/identifier new file mode 100644 index 0000000000..357df5354f --- /dev/null +++ b/tools/tests/apps/package-stats-tests/.meteor/identifier @@ -0,0 +1 @@ +l0mpu1sq7jnnxmfesk \ No newline at end of file diff --git a/tools/tests/apps/package-stats-tests/.meteor/packages b/tools/tests/apps/package-stats-tests/.meteor/packages new file mode 100644 index 0000000000..2b350e8d94 --- /dev/null +++ b/tools/tests/apps/package-stats-tests/.meteor/packages @@ -0,0 +1,5 @@ +# Meteor packages used by this project, one per line. +# +# 'meteor add' and 'meteor remove' will edit this file for you, +# but you can also edit it by hand. +local-package \ No newline at end of file diff --git a/tools/tests/apps/package-stats-tests/.meteor/release b/tools/tests/apps/package-stats-tests/.meteor/release new file mode 100644 index 0000000000..621e94f0ec --- /dev/null +++ b/tools/tests/apps/package-stats-tests/.meteor/release @@ -0,0 +1 @@ +none diff --git a/tools/tests/apps/package-stats-tests/package-stats-tests.js b/tools/tests/apps/package-stats-tests/package-stats-tests.js new file mode 100644 index 0000000000..c9fcac2859 --- /dev/null +++ b/tools/tests/apps/package-stats-tests/package-stats-tests.js @@ -0,0 +1,10 @@ +// This app functions with no packages loaded, so it's good for testing releases +// with no packages. All it does is print "RUNNING" and run forever. +main = function () { + // Tell the runner we're up. + console.log("LISTENING"); + // Ensure Node doesn't kill us. + process.stdin.resume(); + // Ensure boot.js doesn't kill us. + return 'DAEMON'; +}; diff --git a/tools/tests/apps/package-stats-tests/packages/local-package/blah.js b/tools/tests/apps/package-stats-tests/packages/local-package/blah.js new file mode 100644 index 0000000000..f2eb4b3e6f --- /dev/null +++ b/tools/tests/apps/package-stats-tests/packages/local-package/blah.js @@ -0,0 +1 @@ +console.log("BLAH"); diff --git a/tools/tests/apps/package-stats-tests/packages/local-package/package.js b/tools/tests/apps/package-stats-tests/packages/local-package/package.js new file mode 100644 index 0000000000..83a22bb181 --- /dev/null +++ b/tools/tests/apps/package-stats-tests/packages/local-package/package.js @@ -0,0 +1,8 @@ +Package.describe({ + summary: "a package", + version: "1.0.0" +}); + +Package.on_use(function (api) { + api.add_files("blah.js"); +}); diff --git a/tools/tests/apps/package-stats-tests/packages/package-stats-opt-out/package.js b/tools/tests/apps/package-stats-tests/packages/package-stats-opt-out/package.js new file mode 100644 index 0000000000..3dfc770eb1 --- /dev/null +++ b/tools/tests/apps/package-stats-tests/packages/package-stats-opt-out/package.js @@ -0,0 +1,9 @@ +// This is a replica of the core package-stats-opt-out package. It +// exists to make it so that we can add package-stats-opt-out to this +// app without needing to be using a release that has core packages in +// it (such as using a sandbox created with a `warehouse` argument). + +Package.describe({ + summary: "a replica of a core package", + version: "1.0.0" +}); diff --git a/tools/tests/apps/package-stats-tests/programs/empty-control/attributes.json b/tools/tests/apps/package-stats-tests/programs/empty-control/attributes.json new file mode 100644 index 0000000000..11108c5002 --- /dev/null +++ b/tools/tests/apps/package-stats-tests/programs/empty-control/attributes.json @@ -0,0 +1 @@ +{"isControlProgram": true} diff --git a/tools/tests/apps/package-stats-tests/programs/empty-control/package.js b/tools/tests/apps/package-stats-tests/programs/empty-control/package.js new file mode 100644 index 0000000000..ceb498734a --- /dev/null +++ b/tools/tests/apps/package-stats-tests/programs/empty-control/package.js @@ -0,0 +1,4 @@ +// Empty program. Means we don't need a ctl package. +Package.describe({ + version: "1.0.0" +}); diff --git a/tools/tests/report-stats.js b/tools/tests/report-stats.js index d7f3ef5da6..4230897ebf 100644 --- a/tools/tests/report-stats.js +++ b/tools/tests/report-stats.js @@ -15,93 +15,147 @@ process.env.METEOR_PACKAGE_STATS_SERVER_URL = testStatsServer; // than 30 minutes. This is because the `fetchAppPackageUsage` method // works by passing an hour time range. selftest.define("report-stats", ["slow"], function () { - var s = new Sandbox; + _.each( + // If we are currently running from a checkout, then we run this + // test twice (once in which the sandbox uses the current checkout, + // and another in which the sandbox uses a simulated release). If we + // are currently running from a release, then we can only have the + // sandbox use a simulated release that is the same as our current + // release (we can't simulate a checkout). + release.current.isCheckout() ? [true, false] : [false], + function (useFakeRelease) { + var sandboxOpts; + if (useFakeRelease) { + sandboxOpts = { + warehouse: { + v1: { recommended: true } + } + }; + } + var s = new Sandbox(sandboxOpts); + var run; - var run = s.run("create", "foo"); - run.expectExit(0); - s.cd("foo"); + if (useFakeRelease) { + // This makes packages not depend on meteor (specifically, makes + // our empty control program not depend on meteor). We don't + // want to depend on meteor when running from a fake release, + // because fake releases don't have any packages. + s.set("NO_METEOR_PACKAGE", "t"); + } - project.project.setRootDir(s.cwd); + s.createApp("foo", "package-stats-tests"); + s.cd("foo"); + if (useFakeRelease) { + s.write('.meteor/release', 'METEOR-CORE@v1'); + } - // verify that identifier file exists for new apps - var identifier = s.read(".meteor/identifier"); - selftest.expectEqual(!! identifier, true); - selftest.expectEqual(identifier.length > 0, true); + var sandboxProject = new project.Project(); + sandboxProject.setRootDir(s.cwd); - // verify that identifier file when running 'meteor bundle' on apps - // with no identifier file (eg pre-0.9.0 apps) - bundleWithFreshIdentifier(s); - identifier = s.read(".meteor/identifier"); - selftest.expectEqual(!! identifier, true); - selftest.expectEqual(identifier.length > 0, true); + // XXX test that local-package is a direct dep - // we just ran 'meteor bundle' so let's test that we actually sent - // package usage stats - var usage = fetchPackageUsageForApp(identifier); - selftest.expectEqual(_.sortBy(usage.packages, "name"), - _.sortBy(stats.packageList(), "name")); + // verify that identifier file exists for new apps + var identifier = s.read(".meteor/identifier"); + selftest.expectEqual(!! identifier, true); + selftest.expectEqual(identifier.length > 0, true); - // verify that the stats server recorded that with no userId - var appPackages = stats.getPackagesForAppIdInTest(); - selftest.expectEqual(appPackages.appId, identifier); - selftest.expectEqual(appPackages.userId, null); - selftest.expectEqual(_.sortBy(appPackages.packages, "name"), - _.sortBy(stats.packageList(), "name")); + // verify that identifier file when running 'meteor bundle' on apps + // with no identifier file (eg pre-0.9.0 apps) + bundleWithFreshIdentifier(s, sandboxProject); - // now bundle again while logged in. verify that the stats server - // recorded that with the right userId - testUtils.login(s, "test", "testtest"); - bundleWithFreshIdentifier(s); - appPackages = stats.getPackagesForAppIdInTest(); - selftest.expectEqual(appPackages.userId, testUtils.getUserId(s)); + identifier = s.read(".meteor/identifier"); + selftest.expectEqual(!! identifier, true); + selftest.expectEqual(identifier.length > 0, true); - var expectedUserAgentInfo = { - hostname: os.hostname(), - osPlatform: os.platform(), - osType: os.type(), - osRelease: os.release(), - osArch: os.arch() - }; - if (! release.current.isCheckout()) { - expectedUserAgentInfo.meteorReleaseTrack = release.getReleaseTrack(); - expectedUserAgentInfo.meteorReleaseVersion = release.getReleaseVersion(); - expectedUserAgentInfo.meteorToolsPackageWithVersion = - release.getToolsPackageAtVersion(); - } + // we just ran 'meteor bundle' so let's test that we actually sent + // package usage stats + var usage = fetchPackageUsageForApp(identifier); + selftest.expectEqual(_.sortBy(usage.packages, "name"), + _.sortBy(stats.packageList(sandboxProject), "name")); - selftest.expectEqual(appPackages.meta, expectedUserAgentInfo); + // verify that the stats server recorded that with no userId + var appPackages = stats.getPackagesForAppIdInTest(sandboxProject); + selftest.expectEqual(appPackages.appId, identifier); + selftest.expectEqual(appPackages.userId, null); + selftest.expectEqual(_.sortBy(appPackages.packages, "name"), + _.sortBy(stats.packageList(sandboxProject), "name")); - // Add the opt-out package, verify that no stats are recorded for the - // app. - run = s.run("add", "package-stats-opt-out"); - run.waitSecs(15); - run.expectExit(0); - bundleWithFreshIdentifier(s); - appPackages = stats.getPackagesForAppIdInTest(); - selftest.expectEqual(appPackages, undefined); + // now bundle again while logged in. verify that the stats server + // recorded that with the right userId + testUtils.login(s, "test", "testtest"); + bundleWithFreshIdentifier(s, sandboxProject); + appPackages = stats.getPackagesForAppIdInTest(sandboxProject); + selftest.expectEqual(appPackages.userId, testUtils.getUserId(s)); - // Remove the opt-out package, verify that stats get sent again. - run = s.run("remove", "package-stats-opt-out"); - run.waitSecs(15); - run.expectExit(0); - bundle(s); - appPackages = stats.getPackagesForAppIdInTest(); - selftest.expectEqual(appPackages.userId, testUtils.getUserId(s)); - selftest.expectEqual(_.sortBy(appPackages.packages, "name"), - _.sortBy(stats.packageList(), "name")); + var expectedUserAgentInfo = { + hostname: os.hostname(), + osPlatform: os.platform(), + osType: os.type(), + osRelease: os.release(), + osArch: os.arch() + }; + if (useFakeRelease) { + expectedUserAgentInfo.meteorReleaseTrack = + "METEOR-CORE"; + expectedUserAgentInfo.meteorReleaseVersion = + "v1"; + + // Check the tools package version against a regexp and then + // delete it from `appPackages.meta` so that we can check the + // rest of `appPackages.meta` with `expectEqual`. + var toolsPackageWithVersion = + appPackages.meta.meteorToolsPackageWithVersion; + if (! toolsPackageWithVersion.match( + /meteor-tool@\d.\d.\d(\+[a-z0-9]+)?/)) { + selftest.fail(); + } + delete appPackages.meta.meteorToolsPackageWithVersion; + } + + selftest.expectEqual(appPackages.meta, expectedUserAgentInfo); + + // Add the opt-out package, verify that no stats are recorded for the + // app. + // + // XXX The app has a local package-stats-opt-out package in it, so + // that we can add the opt-out package without needing to be using + // a release that knows about the opt-out package. (Our sandbox + // release only has the tool package and no others.) In the near + // future we should just have a way to simulate a release in a + // sandbox that knows about all the packages in the meteor release + // or checkout that is running 'meteor self-test'. That will + // simplify this test a lot. + run = s.run("add", "package-stats-opt-out"); + run.waitSecs(15); + run.expectExit(0); + bundleWithFreshIdentifier(s, sandboxProject); + appPackages = stats.getPackagesForAppIdInTest(sandboxProject); + selftest.expectEqual(appPackages, undefined); + + // Remove the opt-out package, verify that stats get sent again. + run = s.run("remove", "package-stats-opt-out"); + run.waitSecs(15); + run.expectExit(0); + bundle(s, sandboxProject); + appPackages = stats.getPackagesForAppIdInTest(sandboxProject); + selftest.expectEqual(appPackages.userId, testUtils.getUserId(s)); + selftest.expectEqual(_.sortBy(appPackages.packages, "name"), + _.sortBy(stats.packageList(sandboxProject), "name")); + } + ); }); // Bundle the app in the current working directory after deleting its // identifier file (meaning a new one will be created). // @param s {Sandbox} -var bundleWithFreshIdentifier = function (s) { +var bundleWithFreshIdentifier = function (s, sandboxProject) { s.unlink(".meteor/identifier"); - bundle(s); + bundle(s, sandboxProject); }; // Bundle the app in the current working directory. // @param s {Sandbox} -var bundle = function (s) { +var bundle = function (s, sandboxProject) { var run = s.run("bundle", "foo.tar.gz"); run.waitSecs(30); run.expectExit(0); @@ -109,7 +163,7 @@ var bundle = function (s) { // XXX not sure why this is necessary (i.e. why project can't detect // that .meteor/identifier or .meteor/packages has changed and figure // out that it needs to reload itself) - project.project.reload(); + sandboxProject.reload(); }; // Contact the package stats server and look for a given app