diff --git a/.circleci/config.yml b/.circleci/config.yml index f3474cfcd3..38c0968f0e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -17,6 +17,10 @@ run_env_change: &run_env_change sudo mkdir -p /tmp/core_dumps sudo chmod a+rwx /tmp/core_dumps + # Make a place for JUnit tests to live. + sudo mkdir -p /tmp/results/junit + sudo chmod -R a+rwx /tmp/results/ + # Set the pattern for core dumps, so we can find them. echo kernel.core_pattern="/tmp/core_dumps/core.%e.%p.%h.%t" | \ sudo tee -a /etc/sysctl.conf @@ -167,6 +171,7 @@ jobs: ./meteor self-test \ --exclude "${SELF_TEST_EXCLUDE}" \ --headless \ + --junit /tmp/results/junit/0.xml \ --with-tag "custom-warehouse" no_output_timeout: 20m - run: @@ -174,6 +179,10 @@ jobs: - save_cache: key: meteor-cache <<: *meteor_cache_dirs + - store_test_results: + path: /tmp/results + - store_artifacts: + path: /tmp/results - store_artifacts: path: /tmp/core_dumps - store_artifacts: @@ -195,6 +204,7 @@ jobs: ./meteor self-test \ --exclude "${SELF_TEST_EXCLUDE}" \ --headless \ + --junit /tmp/results/junit/1.xml \ --file '^[a-b]|^c[a-n]|^co[a-l]|^compiler-plugins' \ --without-tag "custom-warehouse" no_output_timeout: 20m @@ -203,6 +213,10 @@ jobs: - save_cache: key: meteor-cache <<: *meteor_cache_dirs + - store_test_results: + path: /tmp/results + - store_artifacts: + path: /tmp/results - store_artifacts: path: /tmp/core_dumps - store_artifacts: @@ -224,6 +238,7 @@ jobs: ./meteor self-test \ --exclude "${SELF_TEST_EXCLUDE}" \ --headless \ + --junit /tmp/results/junit/2.xml \ --file "^co[n-z]|^c[p-z]|^[d-k]" \ --without-tag "custom-warehouse" no_output_timeout: 20m @@ -232,6 +247,10 @@ jobs: - save_cache: key: meteor-cache <<: *meteor_cache_dirs + - store_test_results: + path: /tmp/results + - store_artifacts: + path: /tmp/results - store_artifacts: path: /tmp/core_dumps - store_artifacts: @@ -253,6 +272,7 @@ jobs: ./meteor self-test \ --exclude "${SELF_TEST_EXCLUDE}" \ --headless \ + --junit /tmp/results/junit/3.xml \ --file '^[l-o]' \ --without-tag "custom-warehouse" no_output_timeout: 20m @@ -261,6 +281,10 @@ jobs: - save_cache: key: meteor-cache <<: *meteor_cache_dirs + - store_test_results: + path: /tmp/results + - store_artifacts: + path: /tmp/results - store_artifacts: path: /tmp/core_dumps - store_artifacts: @@ -282,6 +306,7 @@ jobs: ./meteor self-test \ --exclude "${SELF_TEST_EXCLUDE}" \ --headless \ + --junit /tmp/results/junit/4.xml \ --file '^p' \ --without-tag "custom-warehouse" no_output_timeout: 20m @@ -290,6 +315,10 @@ jobs: - save_cache: key: meteor-cache <<: *meteor_cache_dirs + - store_test_results: + path: /tmp/results + - store_artifacts: + path: /tmp/results - store_artifacts: path: /tmp/core_dumps - store_artifacts: @@ -311,6 +340,7 @@ jobs: ./meteor self-test \ --exclude "${SELF_TEST_EXCLUDE}" \ --headless \ + --junit /tmp/results/junit/5.xml \ --file '^run' \ --without-tag "custom-warehouse" no_output_timeout: 20m @@ -319,6 +349,10 @@ jobs: - save_cache: key: meteor-cache <<: *meteor_cache_dirs + - store_test_results: + path: /tmp/results + - store_artifacts: + path: /tmp/results - store_artifacts: path: /tmp/core_dumps - store_artifacts: @@ -340,6 +374,7 @@ jobs: ./meteor self-test \ --exclude "${SELF_TEST_EXCLUDE}" \ --headless \ + --junit /tmp/results/junit/6.xml \ --file '^r(?!un)|^s' \ --without-tag "custom-warehouse" no_output_timeout: 20m @@ -348,6 +383,10 @@ jobs: - save_cache: key: meteor-cache <<: *meteor_cache_dirs + - store_test_results: + path: /tmp/results + - store_artifacts: + path: /tmp/results - store_artifacts: path: /tmp/core_dumps - store_artifacts: @@ -369,6 +408,7 @@ jobs: ./meteor self-test \ --exclude "${SELF_TEST_EXCLUDE}" \ --headless \ + --junit /tmp/results/junit/7.xml \ --file '^[t-z]|^command-line' \ --without-tag "custom-warehouse" no_output_timeout: 20m @@ -377,6 +417,10 @@ jobs: - save_cache: key: meteor-cache <<: *meteor_cache_dirs + - store_test_results: + path: /tmp/results + - store_artifacts: + path: /tmp/results - store_artifacts: path: /tmp/core_dumps - store_artifacts: diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 0595ee0728..fef4464c79 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,20 +1,32 @@ -**NOTE:** Issues in this repository are reserved for bugs only. To submit a feature request, please open a new issue in the [meteor/meteor-feature-requests](https://github.com/meteor/meteor-feature-requests) repository. + diff --git a/Roadmap.md b/Roadmap.md index c131efa242..ab4c853084 100644 --- a/Roadmap.md +++ b/Roadmap.md @@ -2,7 +2,7 @@ # Meteor Roadmap -**Up to date as of March 17, 2017** +**Up to date as of August 11, 2017** This document describes the high level features the Meteor project maintainers have decided to prioritize in the near- to medium-term future. A large fraction of the maintainers’ time will be dedicated to working on the features described here. As with any roadmap, this is a living document that will evolve as priorities and dependencies shift; we aim to update the roadmap with any changes or status updates on a monthly basis. @@ -10,25 +10,64 @@ Contributors are encouraged to focus their efforts on work that aligns with the Items can be added to this roadmap by first getting design approval for a solution to an open issue, as outlined by our [contributing guidelines](https://github.com/meteor/meteor/blob/devel/Contributing.md). Then, when a contributor has committed to solving the issue in the short to medium term, they can submit a PR to add that work to the roadmap. All other PRs to the roadmap will be rejected. +## Upgrade to Node 8 + +*Tracking pull request: https://github.com/meteor/meteor/pull/8728* + +Upgrading Node will allow Meteor to take better advantage of native support for new ECMAScript features on the server, which should speed up build performance and also improve runtime performance, thanks to performance improvements in Node itself. + +Perhaps even more importantly, newer versions of Node support a vastly improved debugging experience. Not only can you use native Chrome DevTools and many other debugging clients (WebStorm, VS Code, etc.) to debug your app (no more [`node-inspector`](https://www.npmjs.com/package/node-inspector)), but also the Node process runs at full speed while debugging, so you don't have to wait as long for problems to manifest themselves. + +## Out of the box support for advanced React features + +React is the most popular way to build UIs in JavaScript today, and a great companion to the rest of the features provided by Meteor. Meteor's zero-configuration environment provides a great opportunity to make features React apps depend on work out of the box. This includes features like: + +1. Automatic selection of development vs. production build of React (completed) +2. Abstraction for isomorphic server-side rendering ([ongoing](https://github.com/meteor/meteor/blob/devel/packages/server-render/README.md)) +3. Integration of [dynamic imports](https://blog.meteor.com/dynamic-imports-in-meteor-1-5-c6130419c3cd) with React SSR +4. Full support for optimized CSS-in-JS features of libraries like [styled-components](https://www.styled-components.com/) + +We think Meteor has a clear set of benefits when compared to other popular React frameworks like Create React App and Next.js. + +## Remove blockers to Meteor adoption + +### Support the latest version of Node + +*Tracking pull request: https://github.com/meteor/meteor/pull/8728* + +See [above](https://github.com/meteor/meteor/blob/devel/Roadmap.md#upgrade-to-node-8). Developers deserve to use the latest underlying technologies, and Meteor is uniquely able to smooth over any rough edges in early/experimental versions of technologies like Node. A number of developers are already using beta versions of Meteor 1.6 to deploy their apps, because the benefits outweigh the risks for them. Just as Meteor 1.5 climbed to more than 50% usage in less than two months, we expect Meteor 1.6 to become the most widely used version of Meteor soon after its release. + +### Make Mongo more optional + +*Preliminary solution: https://github.com/meteor/meteor/pull/8999* + +Meteor has depended on Mongo for as long as the Meteor project has existed. However, we care deeply about supporting other data storage systems (especially via [GraphQL](https://www.apollodata.com/)), and would like to make it possible to avoid using Mongo altogether. + +### Get rid of the `imports` directory + +When Meteor 1.3 first introduced a module system based on [CommonJS](http://wiki.commonjs.org/wiki/Modules/1.1) and [ECMAScript module syntax](2ality.com/2014/09/es6-modules-final.html), we had to provide a way for developers to migrate their apps from the old ways of loading code, whereby all files were evaluated eagerly during application startup. + +The best solution at the time was to introduce a special `imports` directory to contain modules that should be loaded lazily (rather than eagerly), when first imported. + +Most other Node applications work this way by default: every module is lazy, and therefore must be imported by another module, and evaluation starts with one "entry point" module (typically specified by the `"main"` field in `package.json`). + +It should be possible for Meteor apps to opt into this behavior, and optionally get rid of their special `imports` directories. The mechanism for opting in will very likely involve putting something in your `package.json` file that specifies entry point modules for both client and server. + +### Make the `meteor` command-line tool installable from npm + +Installing `meteor` from npm would enable developers to use it as build tool for npm-based projects, and would simplify the Meteor release process by getting rid of the "dev bundle" (essentially the npm dependencies of the command-line tool). + +The biggest blockers to this project are + +1. deciding whether/how to preserve Meteor release versions, and +2. changing the API of the package server so that you don't have to download the entire package database locally. ## Page load performance improvements -*Tracking pull request: https://github.com/meteor/meteor/pull/8327* - -Just as Meteor 1.4.2 took aim at rebuild performance, Meteor 1.5 will be all about production app performance, specifically client-side application startup time. - -Fast initial page load times are somewhat less important for single-page reactive web apps than for other kinds of websites, but large Meteor apps with lots of packages tend to load quite a bit of JavaScript, and the cost of all that network traffic, parsing, and evaluation definitely adds up. +*Ongoing* Speeding up page load times will require a combination of new tools for asynchronous JavaScript delivery (code splitting), dead code elimination, deferred evaluation of JavaScript modules, and performance profiling (so that developers can identify expensive packages). -### Dynamic `import(...)` - -The banner feature of this effort will be first-class support for [dynamic `import(...)`](https://github.com/tc39/proposal-dynamic-import), which enables asynchronous module fetching. - -Read the recent [blog post](https://blog.meteor.com/meteor-1-5-react-loadable-f029a320e59c) for an overview of how this system will work in Meteor 1.5. - -Remaining work can be found [here](https://github.com/meteor/meteor/blob/release-1.5/packages/dynamic-import/TODO.md), though not all of those ideas will necessarily block the initial 1.5 release. - ### Making large dependencies optional Making it possible to remove (or dynamically load) large dependencies like `jquery`, `underscore`, and `minimongo` will have a significant impact on bundle sizes. @@ -43,12 +82,6 @@ Dynamic `import(...)` benefits dramatically from storing previously-received mod Although Meteor minifies JavaScript in production, and modules that are never imported are not included in the client bundle, Meteor could do a better job of removing code within modules that will never be used, according to static analysis. The static syntax of ECMAScript 2015 `import` and `export` declarations should make this analysis easier. -## Upgrade to Node 6 - -*Tracking pull request: https://github.com/meteor/meteor/pull/6923* - -Upgrading Node will allow Meteor to take better advantage of native support for new ECMAScript features on the server, which should speed up build performance and may also improve runtime performance, thanks to performance improvements in Node itself. - ## Full transition to npm @@ -73,6 +106,15 @@ Even though Apollo could eventually be a complete replacement for Meteor’s inc # **Recently completed** +## Dynamic `import(...)` + +*Status: Shipped in 1.5* + +The banner feature of this effort will be first-class support for [dynamic `import(...)`](https://github.com/tc39/proposal-dynamic-import), which enables asynchronous module fetching. + +Read the recent [blog post](https://blog.meteor.com/meteor-1-5-react-loadable-f029a320e59c) for an overview of how this system will work in Meteor 1.5. + +Remaining work can be found [here](https://github.com/meteor/meteor/blob/release-1.5/packages/dynamic-import/TODO.md), though not all of those ideas will necessarily block the initial 1.5 release. ## Rebuild performance improvements diff --git a/packages/modules/package.js b/packages/modules/package.js index 479325548b..3d5e5b80e6 100644 --- a/packages/modules/package.js +++ b/packages/modules/package.js @@ -1,6 +1,6 @@ Package.describe({ name: "modules", - version: "0.9.2", + version: "0.9.4", summary: "CommonJS module system", documentation: "README.md" }); @@ -14,5 +14,4 @@ Package.onUse(function(api) { api.mainModule("client.js", "client"); api.mainModule("server.js", "server"); api.export("meteorInstall"); - api.export("process"); }); diff --git a/packages/modules/process.js b/packages/modules/process.js index 14925590f7..949a000291 100644 --- a/packages/modules/process.js +++ b/packages/modules/process.js @@ -1,32 +1,36 @@ -try { - // The application can run `npm install process` to provide its own - // process stub; otherwise this module will provide a partial stub. - process = global.process || require("process"); -} catch (noProcess) { - process = {}; +if (! global.process) { + try { + // The application can run `npm install process` to provide its own + // process stub; otherwise this module will provide a partial stub. + global.process = require("process"); + } catch (missing) { + global.process = {}; + } } +var proc = global.process; + if (Meteor.isServer) { // Make require("process") work on the server in all versions of Node. meteorInstall({ node_modules: { "process.js": function (r, e, module) { - module.exports = process; + module.exports = proc; } } }); } else { - process.platform = "browser"; - process.nextTick = process.nextTick || Meteor._setImmediate; + proc.platform = "browser"; + proc.nextTick = proc.nextTick || Meteor._setImmediate; } -if (typeof process.env !== "object") { - process.env = {}; +if (typeof proc.env !== "object") { + proc.env = {}; } var hasOwn = Object.prototype.hasOwnProperty; for (var key in meteorEnv) { if (hasOwn.call(meteorEnv, key)) { - process.env[key] = meteorEnv[key]; + proc.env[key] = meteorEnv[key]; } } diff --git a/tools/cli/commands.js b/tools/cli/commands.js index 09b4387dcb..4d76c2b420 100644 --- a/tools/cli/commands.js +++ b/tools/cli/commands.js @@ -2079,6 +2079,7 @@ main.registerCommand({ 'without-tag': { type: String }, // Only run tests with this tag 'with-tag': { type: String }, + junit: { type: String }, }, hidden: true, catalogRefresh: new catalog.Refresh.Never() @@ -2175,6 +2176,7 @@ main.registerCommand({ // other options historyLines: options.history, clients: clients, + junit: options.junit && files.pathResolve(options.junit), 'without-tag': options['without-tag'], 'with-tag': options['with-tag'] }); diff --git a/tools/console/console.js b/tools/console/console.js index f04c2bfd70..4501ae3473 100644 --- a/tools/console/console.js +++ b/tools/console/console.js @@ -584,6 +584,10 @@ class Console extends ConsoleBase { }); } + isInteractive() { + return !this._headless; + } + setPretty(pretty) { // If we're being forced, do nothing. if (FORCE_PRETTY !== undefined) { diff --git a/tools/fs/files.js b/tools/fs/files.js index b9b618476a..1dbb1de538 100644 --- a/tools/fs/files.js +++ b/tools/fs/files.js @@ -1557,6 +1557,9 @@ function wrapFsFunc(fsFuncName, pathArgIndices, options) { fsFuncName === 'rename' || fsFuncName === 'symlink'); + const dirty = options && options.dirty; + const dirtyFn = typeof dirty === "function" ? dirty : null; + if (canYield() && shouldBeSync && ! isQuickie) { @@ -1576,6 +1579,10 @@ function wrapFsFunc(fsFuncName, pathArgIndices, options) { const result = promise.await(); + if (dirtyFn) { + dirtyFn(...args); + } + return options.modifyReturnValue ? options.modifyReturnValue(result) : result; @@ -1584,6 +1591,11 @@ function wrapFsFunc(fsFuncName, pathArgIndices, options) { // Should be sync but can't yield: we are not in a Fiber. // Run the sync version of the fs.* method. const result = fsFuncSync.apply(fs, args); + + if (dirtyFn) { + dirtyFn(...args); + } + return options.modifyReturnValue ? options.modifyReturnValue(result) : result; @@ -1600,12 +1612,20 @@ function wrapFsFunc(fsFuncName, pathArgIndices, options) { args.push((err, res) => { err ? reject(err) : resolve(res); }); + fsFunc.apply(fs, args); + }).then(res => { + if (dirtyFn) { + dirtyFn(...args); + } + if (options.modifyReturnValue) { res = options.modifyReturnValue(res); } + cb && cb(null, res); + }, cb); return; @@ -1626,8 +1646,27 @@ function wrapFsFunc(fsFuncName, pathArgIndices, options) { Profile('wrapped.fs.' + fsFuncName + 'Sync', makeWrapper({ sync: true })); } -wrapFsFunc("writeFile", [0]); -wrapFsFunc("appendFile", [0]); +let dependOnPathSalt = 0; +export const dependOnPath = require("optimism").wrap( + // Always return something different to prevent optimism from + // second-guessing the dirtiness of this function. + path => ++dependOnPathSalt +); + +function wrapDestructiveFsFunc(name, pathArgIndices) { + pathArgIndices = pathArgIndices || [0]; + wrapFsFunc(name, pathArgIndices, { + dirty(...args) { + // Immediately reset all optimistic functions (defined in + // tools/fs/optimistic.js) that depend on these paths. + pathArgIndices.forEach(i => dependOnPath.dirty(args[i])); + } + }); +} + +wrapDestructiveFsFunc("writeFile"); +wrapDestructiveFsFunc("appendFile"); + wrapFsFunc("readFile", [0], { modifyReturnValue: function (fileData) { if (_.isString(fileData)) { @@ -1637,9 +1676,11 @@ wrapFsFunc("readFile", [0], { return fileData; } }); + wrapFsFunc("stat", [0]); wrapFsFunc("lstat", [0]); -wrapFsFunc("rename", [0, 1]); + +wrapDestructiveFsFunc("rename", [0, 1]); // After the outermost files.withCache call returns, the withCacheCache is // reset to null so that it does not survive server restarts. @@ -1748,10 +1789,11 @@ wrapFsFunc("readdir", [0], { } }); -wrapFsFunc("rmdir", [0]); -wrapFsFunc("mkdir", [0]); -wrapFsFunc("unlink", [0]); -wrapFsFunc("chmod", [0]); +wrapDestructiveFsFunc("rmdir"); +wrapDestructiveFsFunc("mkdir"); +wrapDestructiveFsFunc("unlink"); +wrapDestructiveFsFunc("chmod"); + wrapFsFunc("open", [0]); // XXX this doesn't give you the second argument to the callback diff --git a/tools/fs/optimistic.js b/tools/fs/optimistic.js index 902030d11f..f54b61f58f 100644 --- a/tools/fs/optimistic.js +++ b/tools/fs/optimistic.js @@ -10,6 +10,7 @@ import { lstat, readFile, readdir, + dependOnPath, } from "./files.js"; // When in doubt, the optimistic caching system can be completely disabled @@ -18,7 +19,7 @@ const ENABLED = ! process.env.METEOR_DISABLE_OPTIMISTIC_CACHING; function makeOptimistic(name, fn) { const wrapper = wrap(ENABLED ? function (...args) { - maybeDependOnNodeModules(args[0]); + maybeDependOnPath(args[0]); return fn.apply(this, args); } : fn, { makeCacheKey(...args) { @@ -115,6 +116,13 @@ export const shouldWatch = wrap(path => { return false; }); +function maybeDependOnPath(path) { + if (typeof path === "string") { + dependOnPath(path); + maybeDependOnNodeModules(path); + } +} + function maybeDependOnNodeModules(path) { if (typeof path !== "string") { return; diff --git a/tools/meteor-services/deploy.js b/tools/meteor-services/deploy.js index e0ff7afcaa..b4f6148730 100644 --- a/tools/meteor-services/deploy.js +++ b/tools/meteor-services/deploy.js @@ -179,6 +179,15 @@ function authedRpc(options) { delete rpcOptions.printDeployURL; if (infoResult.statusCode === 401 && rpcOptions.promptIfAuthFails) { + Console.error("Authentication failed or login token expired."); + + if (!Console.isInteractive()) { + return { + statusCode: 401, + errorMessage: "login failed." + }; + } + // Our authentication didn't validate, so prompt the user to log in // again, and resend the RPC if the login succeeds. var username = Console.readLine({ diff --git a/tools/tests/command-line.js b/tools/tests/command-line.js index 5e42c26444..3c9336e9a4 100644 --- a/tools/tests/command-line.js +++ b/tools/tests/command-line.js @@ -430,7 +430,7 @@ selftest.define("old cli tests (converted)", function () { run = s.run("remove", "--help"); run.match("Removes a package"); run = s.run("list", "--help"); - run.match("This will not list transitive dependencies"); + run.match("Transitive dependencies are not listed unless"); run = s.run("bundle", "--help"); run.match("command has been deprecated"); run = s.run("build", "--help"); diff --git a/tools/tests/compiler-plugins.js b/tools/tests/compiler-plugins.js index ad6735ce2f..aa2d43a6ee 100644 --- a/tools/tests/compiler-plugins.js +++ b/tools/tests/compiler-plugins.js @@ -22,7 +22,6 @@ function startRun(sandbox) { // Tests the actual cache logic used by coffeescript. selftest.define("compiler plugin caching - coffee", () => { var s = new Sandbox({ fakeMongo: true }); - s.set("METEOR_WATCH_PRIORITIZE_CHANGED", "false"); s.createApp("myapp", "caching-coffee"); s.cd("myapp"); @@ -92,7 +91,6 @@ selftest.define("compiler plugin caching - coffee", () => { selftest.define("compiler plugin caching - " + packageName, () => { var s = new Sandbox({ fakeMongo: true }); - s.set("METEOR_WATCH_PRIORITIZE_CHANGED", "false"); s.createApp("myapp", "caching-" + packageName); s.cd("myapp"); @@ -280,7 +278,6 @@ selftest.define("compiler plugin caching - local plugin", function () { // Test error on duplicate compiler plugins. selftest.define("compiler plugins - duplicate extension", () => { const s = new Sandbox({ fakeMongo: true }); - s.set("METEOR_WATCH_PRIORITIZE_CHANGED", "false"); s.createApp("myapp", "duplicate-compiler-extensions"); s.cd("myapp"); diff --git a/tools/tests/cordova-plugins.js b/tools/tests/cordova-plugins.js index b199b556a2..3c4e62a152 100644 --- a/tools/tests/cordova-plugins.js +++ b/tools/tests/cordova-plugins.js @@ -136,8 +136,6 @@ selftest.define("change cordova plugins", ["cordova"], function () { var s = new Sandbox(); var run; - s.set("METEOR_WATCH_PRIORITIZE_CHANGED", "false"); - // Starting a run s.createApp("myapp", "package-tests"); s.cd("myapp"); diff --git a/tools/tests/package-tests.js b/tools/tests/package-tests.js index 4985056957..856b3c393a 100644 --- a/tools/tests/package-tests.js +++ b/tools/tests/package-tests.js @@ -100,8 +100,6 @@ selftest.define("change packages during hot code push", [], function () { var s = new Sandbox(); var run; - s.set("METEOR_WATCH_PRIORITIZE_CHANGED", "false"); - // Starting a run s.createApp("myapp", "package-tests"); s.cd("myapp"); diff --git a/tools/tests/run.js b/tools/tests/run.js index 5b0c4cbed4..5a3386ebd0 100644 --- a/tools/tests/run.js +++ b/tools/tests/run.js @@ -22,8 +22,6 @@ selftest.define("run", function () { var s = new Sandbox({ fakeMongo: true }); var run; - s.set("METEOR_WATCH_PRIORITIZE_CHANGED", "false"); - // Starting a run s.createApp("myapp", "standard-app"); s.cd("myapp"); @@ -264,7 +262,7 @@ selftest.define("update during run", ["checkout", 'custom-warehouse'], function run.match('localhost:3000'); s.write('.meteor/release', DEFAULT_RELEASE_TRACK + '@v2'); s.write('empty.js', ''); - run.waitSecs(2); + run.waitSecs(10); run.match('restarted'); run.waitSecs(10); run.stop(); @@ -274,11 +272,11 @@ selftest.define("update during run", ["checkout", 'custom-warehouse'], function s.write('.meteor/release', DEFAULT_RELEASE_TRACK + '@v1'); run = s.run("--release", DEFAULT_RELEASE_TRACK + "@v1"); run.tellMongo(MONGO_LISTENING); - run.waitSecs(2); + run.waitSecs(10); run.match('localhost:3000'); s.write('.meteor/release', DEFAULT_RELEASE_TRACK + '@v2'); s.write('empty.js', ''); - run.waitSecs(2); + run.waitSecs(10); run.match('restarted'); run.waitSecs(10); run.stop(); @@ -292,12 +290,12 @@ selftest.define("update during run", ["checkout", 'custom-warehouse'], function s.write('.meteor/release', DEFAULT_RELEASE_TRACK + '@v1'); run = s.run(); run.tellMongo(MONGO_LISTENING); - run.waitSecs(2); + run.waitSecs(10); run.match('localhost:3000'); run.waitSecs(10); s.write('.meteor/release', DEFAULT_RELEASE_TRACK + '@v2'); s.write('empty.js', ''); - run.waitSecs(2); + run.waitSecs(10); run.match('restarted'); run.waitSecs(10); run.stop(); diff --git a/tools/tool-testing/selftest.js b/tools/tool-testing/selftest.js index 1bf2a21a6b..4935d531d0 100644 --- a/tools/tool-testing/selftest.js +++ b/tools/tool-testing/selftest.js @@ -1,3 +1,4 @@ +import { inspect } from 'util'; import { makeFulfillablePromise } from '../utils/fiber-helpers.js'; import { spawn, execFile } from 'child_process'; import * as files from '../fs/files.js'; @@ -1605,6 +1606,7 @@ class Test { this.fileHash = options.fileHash; this.tags = options.tags || []; this.f = options.func; + this.durationMs = null; this.cleanupHandlers = []; } @@ -1797,6 +1799,16 @@ function getFilteredTests(options) { return new TestList(allTests, tagsToSkip, tagsToMatch, testState); }; +function groupTestsByFile(tests) { + const grouped = {}; + tests.forEach(test => { + grouped[test.file] = grouped[test.file] || []; + grouped[test.file].push(test); + }); + + return grouped; +} + // A TestList is the result of getFilteredTests. It holds the original // list of all tests, the filtered list, and stats on how many tests // were skipped (see generateSkipReport). @@ -1856,8 +1868,120 @@ class TestList { // Mark a test's file as having failures. This prevents // saveTestState from saving its hash as a potentially // "unchanged" file to be skipped in a future run. - notifyFailed(test) { + notifyFailed(test, failureObject) { + // Mark the file that this test lives in as having failures. this.fileInfo[test.file].hasFailures = true; + + // Mark that the specific test failed. + test.failed = true; + + // If there is a failure object, store that for potential output. + if (failureObject) { + test.failureObject = failureObject; + } + } + + saveJUnitOutput(path) { + const grouped = groupTestsByFile(this.filteredTests); + + // We'll form an collection of "testsuites" + const testSuites = []; + + const attrSafe = attr => (attr || "").replace('"', """); + const durationForOutput = durationMs => durationMs / 1000; + + // Each file is a testsuite. + Object.keys(grouped).forEach((file) => { + const testCases = []; + + let countError = 0; + let countFailure = 0; + + // Each test is a "testcase". + grouped[file].forEach((test) => { + const testCaseAttrs = [ + `name="${attrSafe(test.name)}"`, + ]; + + if (test.durationMs) { + testCaseAttrs.push(`time="${durationForOutput(test.durationMs)}"`); + } + + const testCaseAttrsString = testCaseAttrs.join(' '); + + if (test.failed) { + let failureElement = ""; + + if (test.failureObject instanceof TestFailure) { + countFailure++; + + failureElement = [ + ``, + '', + '', + ].join('\n'); + } else if (test.failureObject && test.failureObject.stack) { + countError++; + + failureElement = [ + '', + '', + '', + ].join('\n'); + } else { + countError++; + + failureElement = ''; + } + + testCases.push( + [ + ``, + failureElement, + '', + ].join('\n'), + ); + } else { + testCases.push(``); + } + }); + + const testSuiteAttrs = [ + `name="${file}"`, + `tests="${testCases.length}"`, + `failures="${countFailure}"`, + `errors="${countError}"`, + `time="${durationForOutput(this.durationMs)}"`, + ]; + + const testSuiteAttrsString = testSuiteAttrs.join(' '); + + testSuites.push( + [ + ``, + testCases.join('\n'), + '', + ].join('\n'), + ); + }); + + const xmlHeader = ''; + + const testSuitesString = testSuites.join('\n'); + + files.writeFile(path, + [ + xmlHeader, + ``, + testSuitesString, + ``, + ].join('\n'), + 'utf8', + ); } // If this TestList was constructed with a testState, @@ -1933,17 +2057,11 @@ export function listTests(options) { return; } - const testsGroupedByFile = {}; - testList.filteredTests.forEach(filteredTest => { - testsGroupedByFile[filteredTest.file] = - testsGroupedByFile[filteredTest.file] || []; + const grouped = groupTestsByFile(testList.filteredTests); - testsGroupedByFile[filteredTest.file].push(filteredTest); - }); - - Object.keys(testsGroupedByFile).forEach((file) => { + Object.keys(grouped).forEach((file) => { Console.rawInfo(file + ':\n'); - testsGroupedByFile[file].forEach((test) => { + grouped[file].forEach((test) => { Console.rawInfo(' - ' + test.name + (test.tags.length ? ' [' + test.tags.join(' ') + ']' : '') + '\n'); @@ -1971,6 +2089,8 @@ export function runTests(options) { return 0; } + testList.startTime = new Date; + let totalRun = 0; const failedTests = []; @@ -1980,6 +2100,9 @@ export function runTests(options) { runTest(test); }); + testList.endTime = new Date; + testList.durationMs = testList.endTime - testList.startTime; + function runTest(test, tries = 3) { let failure = null; let startTime; @@ -1997,6 +2120,8 @@ export function runTests(options) { test.cleanup(); } + test.durationMs = +(new Date) - startTime; + if (failure) { Console.error("... fail!", Console.options({ indent: 2 })); @@ -2009,9 +2134,6 @@ export function runTests(options) { return runTest(test, tries); } - failedTests.push(test); - testList.notifyFailed(test); - if (failure instanceof TestFailure) { const frames = parseStackParse(failure).outsideFiber; const relpath = files.pathRelative(files.getCurrentToolsDir(), @@ -2064,16 +2186,22 @@ export function runTests(options) { } else { Console.rawError(" => Test threw exception: " + failure.stack + "\n"); } + + failedTests.push(test); + testList.notifyFailed(test, failure); } else { - const durationMs = +(new Date) - startTime; Console.error( - "... ok (" + durationMs + " ms)", + "... ok (" + test.durationMs + " ms)", Console.options({ indent: 2 })); } } testList.saveTestState(); + if (options.junit) { + testList.saveJUnitOutput(options.junit); + } + if (totalRun > 0) { Console.error(); }