2020-08-01 03:43:15 +02:00
|
|
|
"use strict";
|
|
|
|
|
2020-10-07 10:48:54 +02:00
|
|
|
const path = require("path");
|
|
|
|
|
2019-07-25 09:13:22 +02:00
|
|
|
const new_globals = new Set();
|
|
|
|
let old_globals = {};
|
2016-07-30 17:00:12 +02:00
|
|
|
|
|
|
|
exports.set_global = function (name, val) {
|
2021-02-22 15:34:23 +01:00
|
|
|
if (val === null) {
|
|
|
|
throw new Error(`
|
|
|
|
We try to avoid using null in our codebase.
|
|
|
|
`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add this for debugging and to allow with_overrides
|
|
|
|
// to know that we're dealing with stubbed code.
|
|
|
|
if (typeof val === "object") {
|
|
|
|
val._patched_with_set_global = true;
|
|
|
|
}
|
|
|
|
|
2019-07-25 09:13:22 +02:00
|
|
|
if (!(name in old_globals)) {
|
|
|
|
if (!(name in global)) {
|
|
|
|
new_globals.add(name);
|
|
|
|
}
|
|
|
|
old_globals[name] = global[name];
|
|
|
|
}
|
2016-07-30 17:00:12 +02:00
|
|
|
global[name] = val;
|
|
|
|
return val;
|
|
|
|
};
|
|
|
|
|
node_tests: Don't remove require cache of module in zrequire.
There is good reason to do this (explanation is bit long!). With the
TypeScript migration, and the require and ES6 migrations that come
with it, we use require instead of set_global which loads the entire
module. Suppose we have a util module, which is used by some other
module, say message_store, and util is being required in message_store
since it is removed from window. Then, if a test zrequires
message_store first, and then zrequires the util module qand mocks one
of its methods, it will not be mocked for the message_store
module. The reason is:
1. zrequire('message_store') leads to require('util').
2. zrequire('util') removes the util module from cache and it is
reloaded. Now the util module in message_store and the one in
the test will be different and any updates to it in tests won't
be reflected in the actual code.
Which can lead to confusion for folks writing tests. I'll mention this
can be avoided doing zrequire('util') first but...that is not ideal.
And, since there was one outlier test that relied on this behavior,
we add the namespace.reset_module function.
2020-08-19 17:35:27 +02:00
|
|
|
function require_path(name, fn) {
|
2017-08-09 18:26:03 +02:00
|
|
|
if (fn === undefined) {
|
2020-07-15 01:29:15 +02:00
|
|
|
fn = "../../static/js/" + name;
|
2019-10-04 23:08:11 +02:00
|
|
|
} else if (/^generated\/|^js\/|^shared\/|^third\//.test(fn)) {
|
2019-06-21 01:59:21 +02:00
|
|
|
// FIXME: Stealing part of the NPM namespace is confusing.
|
2020-07-15 01:29:15 +02:00
|
|
|
fn = "../../static/" + fn;
|
2017-08-09 18:26:03 +02:00
|
|
|
}
|
node_tests: Don't remove require cache of module in zrequire.
There is good reason to do this (explanation is bit long!). With the
TypeScript migration, and the require and ES6 migrations that come
with it, we use require instead of set_global which loads the entire
module. Suppose we have a util module, which is used by some other
module, say message_store, and util is being required in message_store
since it is removed from window. Then, if a test zrequires
message_store first, and then zrequires the util module qand mocks one
of its methods, it will not be mocked for the message_store
module. The reason is:
1. zrequire('message_store') leads to require('util').
2. zrequire('util') removes the util module from cache and it is
reloaded. Now the util module in message_store and the one in
the test will be different and any updates to it in tests won't
be reflected in the actual code.
Which can lead to confusion for folks writing tests. I'll mention this
can be avoided doing zrequire('util') first but...that is not ideal.
And, since there was one outlier test that relied on this behavior,
we add the namespace.reset_module function.
2020-08-19 17:35:27 +02:00
|
|
|
|
|
|
|
return fn;
|
|
|
|
}
|
|
|
|
|
|
|
|
exports.zrequire = function (name, fn) {
|
2021-02-22 19:51:28 +01:00
|
|
|
return require(require_path(name, fn));
|
2017-08-09 18:26:03 +02:00
|
|
|
};
|
|
|
|
|
node_tests: Don't remove require cache of module in zrequire.
There is good reason to do this (explanation is bit long!). With the
TypeScript migration, and the require and ES6 migrations that come
with it, we use require instead of set_global which loads the entire
module. Suppose we have a util module, which is used by some other
module, say message_store, and util is being required in message_store
since it is removed from window. Then, if a test zrequires
message_store first, and then zrequires the util module qand mocks one
of its methods, it will not be mocked for the message_store
module. The reason is:
1. zrequire('message_store') leads to require('util').
2. zrequire('util') removes the util module from cache and it is
reloaded. Now the util module in message_store and the one in
the test will be different and any updates to it in tests won't
be reflected in the actual code.
Which can lead to confusion for folks writing tests. I'll mention this
can be avoided doing zrequire('util') first but...that is not ideal.
And, since there was one outlier test that relied on this behavior,
we add the namespace.reset_module function.
2020-08-19 17:35:27 +02:00
|
|
|
exports.reset_module = function (name, fn) {
|
|
|
|
fn = require_path(name, fn);
|
|
|
|
delete require.cache[require.resolve(fn)];
|
|
|
|
return require(fn);
|
|
|
|
};
|
|
|
|
|
2021-02-22 19:51:28 +01:00
|
|
|
const staticPath = path.resolve(__dirname, "../../static") + path.sep;
|
|
|
|
const templatesPath = staticPath + "templates" + path.sep;
|
2020-02-27 15:40:59 +01:00
|
|
|
|
2016-07-30 17:00:12 +02:00
|
|
|
exports.restore = function () {
|
2021-02-22 19:51:28 +01:00
|
|
|
for (const path of Object.keys(require.cache)) {
|
|
|
|
if (path.startsWith(staticPath) && !path.startsWith(templatesPath)) {
|
|
|
|
delete require.cache[path];
|
|
|
|
}
|
2021-01-22 22:29:08 +01:00
|
|
|
}
|
2020-02-09 04:15:38 +01:00
|
|
|
Object.assign(global, old_globals);
|
2019-07-25 09:13:22 +02:00
|
|
|
old_globals = {};
|
|
|
|
for (const name of new_globals) {
|
|
|
|
delete global[name];
|
|
|
|
}
|
|
|
|
new_globals.clear();
|
2016-07-30 17:00:12 +02:00
|
|
|
};
|
|
|
|
|
2020-07-27 16:06:46 +02:00
|
|
|
exports.with_field = function (obj, field, val, f) {
|
|
|
|
const old_val = obj[field];
|
2021-02-23 04:54:23 +01:00
|
|
|
try {
|
|
|
|
obj[field] = val;
|
|
|
|
return f();
|
|
|
|
} finally {
|
|
|
|
obj[field] = old_val;
|
|
|
|
}
|
2020-07-27 16:06:46 +02:00
|
|
|
};
|
|
|
|
|
2017-03-11 21:07:24 +01:00
|
|
|
exports.with_overrides = function (test_function) {
|
|
|
|
// This function calls test_function() and passes in
|
|
|
|
// a way to override the namespace temporarily.
|
|
|
|
|
2020-07-26 13:21:15 +02:00
|
|
|
const restore_callbacks = [];
|
2020-07-26 13:32:02 +02:00
|
|
|
const unused_funcs = new Map();
|
2021-02-11 14:19:12 +01:00
|
|
|
const funcs = new Map();
|
2017-03-11 21:07:24 +01:00
|
|
|
|
2021-02-12 15:47:26 +01:00
|
|
|
const override = function (obj, func_name, f) {
|
|
|
|
// Given an object `obj` (which is usually a module object),
|
|
|
|
// we re-map `obj[func_name]` to the `f` passed in by the caller.
|
|
|
|
// Then the outer function here (`with_overrides`) automatically
|
|
|
|
// restores the original value of `obj[func_name]` as its last
|
|
|
|
// step. Generally our code calls `run_test`, which wraps
|
|
|
|
// `with_overrides`.
|
2020-07-26 14:31:16 +02:00
|
|
|
if (typeof f !== "function") {
|
2021-02-13 13:20:29 +01:00
|
|
|
throw new TypeError(
|
|
|
|
"You can only override with a function. Use with_field for non-functions.",
|
|
|
|
);
|
2020-07-26 14:31:16 +02:00
|
|
|
}
|
2021-02-22 15:34:23 +01:00
|
|
|
|
|
|
|
if (typeof obj !== "object" && typeof obj !== "function") {
|
|
|
|
throw new TypeError(`We cannot override a function for ${typeof obj} objects`);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (obj[func_name] === undefined) {
|
|
|
|
if (obj !== global.$ && !obj._patched_with_set_global) {
|
|
|
|
throw new Error(`
|
|
|
|
It looks like you are overriding ${func_name}
|
|
|
|
for a module that never defined it, which probably
|
|
|
|
indicates that you have a typo or are doing
|
|
|
|
something hacky in the test.
|
|
|
|
`);
|
|
|
|
}
|
|
|
|
} else if (typeof obj[func_name] !== "function") {
|
|
|
|
throw new TypeError(`
|
|
|
|
You are overriding a non-function with a function.
|
|
|
|
This is almost certainly an error.
|
|
|
|
`);
|
|
|
|
}
|
2020-07-26 14:31:16 +02:00
|
|
|
|
2021-02-12 15:47:26 +01:00
|
|
|
if (!funcs.has(obj)) {
|
|
|
|
funcs.set(obj, new Map());
|
2021-02-11 14:19:12 +01:00
|
|
|
}
|
|
|
|
|
2021-02-12 15:47:26 +01:00
|
|
|
if (funcs.get(obj).has(func_name)) {
|
2021-02-11 14:19:12 +01:00
|
|
|
// Prevent overriding the same function twice, so that
|
|
|
|
// it's super easy to reason about our logic to restore
|
|
|
|
// the original function. Usually if somebody sees this
|
|
|
|
// error, it's a symptom of not breaking up tests enough.
|
2021-02-12 15:41:15 +01:00
|
|
|
throw new Error(
|
|
|
|
"You can only override a function one time. Use with_field for more granular control.",
|
|
|
|
);
|
2021-02-11 14:19:12 +01:00
|
|
|
}
|
|
|
|
|
2021-02-12 15:47:26 +01:00
|
|
|
funcs.get(obj).set(func_name, true);
|
2021-02-11 14:19:12 +01:00
|
|
|
|
2021-02-12 15:47:26 +01:00
|
|
|
if (!unused_funcs.has(obj)) {
|
|
|
|
unused_funcs.set(obj, new Map());
|
2017-03-12 14:32:12 +01:00
|
|
|
}
|
2021-02-11 14:19:12 +01:00
|
|
|
|
2021-02-12 15:47:26 +01:00
|
|
|
unused_funcs.get(obj).set(func_name, true);
|
2017-03-12 14:32:12 +01:00
|
|
|
|
2021-02-22 12:18:06 +01:00
|
|
|
let old_f = obj[func_name];
|
|
|
|
if (old_f === undefined) {
|
|
|
|
// Create a dummy function so that we can
|
|
|
|
// attach _patched_with_override to it.
|
|
|
|
old_f = () => {
|
|
|
|
throw new Error(`There is no ${func_name}() field for this object.`);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
const new_f = function (...args) {
|
2021-02-12 15:47:26 +01:00
|
|
|
unused_funcs.get(obj).delete(func_name);
|
2020-07-26 13:32:02 +02:00
|
|
|
return f.apply(this, args);
|
|
|
|
};
|
2017-03-11 21:07:24 +01:00
|
|
|
|
2021-02-22 12:18:06 +01:00
|
|
|
// Let zjquery know this function was patched with override,
|
|
|
|
// so it doesn't complain about us modifying it. (Other
|
|
|
|
// code can also use this, as needed.)
|
|
|
|
new_f._patched_with_override = true;
|
|
|
|
|
|
|
|
obj[func_name] = new_f;
|
|
|
|
|
2020-07-26 13:21:15 +02:00
|
|
|
restore_callbacks.push(() => {
|
2021-02-22 12:18:06 +01:00
|
|
|
old_f._patched_with_override = true;
|
2021-02-12 15:47:26 +01:00
|
|
|
obj[func_name] = old_f;
|
2021-02-22 12:18:06 +01:00
|
|
|
delete old_f._patched_with_override;
|
2017-03-11 21:07:24 +01:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2021-02-23 04:54:23 +01:00
|
|
|
try {
|
|
|
|
test_function(override);
|
|
|
|
} finally {
|
|
|
|
restore_callbacks.reverse();
|
|
|
|
for (const restore_callback of restore_callbacks) {
|
|
|
|
restore_callback();
|
|
|
|
}
|
js: Automatically convert _.each to for…of.
This commit was automatically generated by the following script,
followed by lint --fix and a few small manual lint-related cleanups.
import * as babelParser from "recast/parsers/babel";
import * as recast from "recast";
import * as tsParser from "recast/parsers/typescript";
import { builders as b, namedTypes as n } from "ast-types";
import { Context } from "ast-types/lib/path-visitor";
import K from "ast-types/gen/kinds";
import { NodePath } from "ast-types/lib/node-path";
import assert from "assert";
import fs from "fs";
import path from "path";
import process from "process";
const checkExpression = (node: n.Node): node is K.ExpressionKind =>
n.Expression.check(node);
const checkStatement = (node: n.Node): node is K.StatementKind =>
n.Statement.check(node);
for (const file of process.argv.slice(2)) {
console.log("Parsing", file);
const ast = recast.parse(fs.readFileSync(file, { encoding: "utf8" }), {
parser: path.extname(file) === ".ts" ? tsParser : babelParser,
});
let changed = false;
let inLoop = false;
let replaceReturn = false;
const visitLoop = (...args: string[]) =>
function(this: Context, path: NodePath) {
for (const arg of args) {
this.visit(path.get(arg));
}
const old = { inLoop };
inLoop = true;
this.visit(path.get("body"));
inLoop = old.inLoop;
return false;
};
recast.visit(ast, {
visitDoWhileStatement: visitLoop("test"),
visitExpressionStatement(path) {
const { expression, comments } = path.node;
let valueOnly;
if (
n.CallExpression.check(expression) &&
n.MemberExpression.check(expression.callee) &&
!expression.callee.computed &&
n.Identifier.check(expression.callee.object) &&
expression.callee.object.name === "_" &&
n.Identifier.check(expression.callee.property) &&
["each", "forEach"].includes(expression.callee.property.name) &&
[2, 3].includes(expression.arguments.length) &&
checkExpression(expression.arguments[0]) &&
(n.FunctionExpression.check(expression.arguments[1]) ||
n.ArrowFunctionExpression.check(expression.arguments[1])) &&
[1, 2].includes(expression.arguments[1].params.length) &&
n.Identifier.check(expression.arguments[1].params[0]) &&
((valueOnly = expression.arguments[1].params[1] === undefined) ||
n.Identifier.check(expression.arguments[1].params[1])) &&
(expression.arguments[2] === undefined ||
n.ThisExpression.check(expression.arguments[2]))
) {
const old = { inLoop, replaceReturn };
inLoop = false;
replaceReturn = true;
this.visit(
path
.get("expression")
.get("arguments")
.get(1)
.get("body")
);
inLoop = old.inLoop;
replaceReturn = old.replaceReturn;
const [right, { body, params }] = expression.arguments;
const loop = b.forOfStatement(
b.variableDeclaration("let", [
b.variableDeclarator(
valueOnly ? params[0] : b.arrayPattern([params[1], params[0]])
),
]),
valueOnly
? right
: b.callExpression(
b.memberExpression(right, b.identifier("entries")),
[]
),
checkStatement(body) ? body : b.expressionStatement(body)
);
loop.comments = comments;
path.replace(loop);
changed = true;
}
this.traverse(path);
},
visitForStatement: visitLoop("init", "test", "update"),
visitForInStatement: visitLoop("left", "right"),
visitForOfStatement: visitLoop("left", "right"),
visitFunction(path) {
this.visit(path.get("params"));
const old = { replaceReturn };
replaceReturn = false;
this.visit(path.get("body"));
replaceReturn = old.replaceReturn;
return false;
},
visitReturnStatement(path) {
if (replaceReturn) {
assert(!inLoop); // could use labeled continue if this ever fires
const { argument, comments } = path.node;
if (argument === null) {
const s = b.continueStatement();
s.comments = comments;
path.replace(s);
} else {
const s = b.expressionStatement(argument);
s.comments = comments;
path.replace(s, b.continueStatement());
}
return false;
}
this.traverse(path);
},
visitWhileStatement: visitLoop("test"),
});
if (changed) {
console.log("Writing", file);
fs.writeFileSync(file, recast.print(ast).code, { encoding: "utf8" });
}
}
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-02-06 06:19:47 +01:00
|
|
|
}
|
2020-07-26 13:32:02 +02:00
|
|
|
|
2021-02-11 01:23:23 +01:00
|
|
|
for (const module_unused_funcs of unused_funcs.values()) {
|
|
|
|
for (const unused_name of module_unused_funcs.keys()) {
|
|
|
|
throw new Error(unused_name + " never got invoked!");
|
|
|
|
}
|
2020-07-26 13:32:02 +02:00
|
|
|
}
|
2017-03-11 21:07:24 +01:00
|
|
|
};
|