zulip/frontend_tests/zjsunit/namespace.js

249 lines
7.2 KiB
JavaScript
Raw Normal View History

"use strict";
const Module = require("module");
const path = require("path");
const new_globals = new Set();
let old_globals = {};
2016-07-30 17:00:12 +02:00
let actual_load;
let objs_installed;
const module_mocks = new Map();
let mocked_paths;
function load(request, parent, isMain) {
const long_fn = path.resolve(path.join(path.dirname(parent.filename), request));
if (module_mocks.has(long_fn)) {
mocked_paths.add(long_fn);
return module_mocks.get(long_fn);
}
return actual_load(request, parent, isMain);
}
exports.start = () => {
if (actual_load !== undefined) {
throw new Error("namespace.start was called twice in a row.");
}
actual_load = Module._load;
Module._load = load;
objs_installed = false;
module_mocks.clear();
mocked_paths = new Set();
};
exports.mock_module = (short_fn, obj) => {
if (obj === undefined) {
obj = {};
}
if (typeof obj !== "object") {
throw new TypeError("We expect you to stub with an object.");
}
if (short_fn.startsWith("/") || short_fn.includes(".")) {
throw new Error(`
There is no need for a path like ${short_fn}.
We just assume the file is under static/js.
`);
}
if (objs_installed) {
throw new Error(`
It is too late to install this mock. Consider instead:
foo.__Rewire__("${short_fn}", ...)
Or call this earlier.
`);
}
const base_path = path.resolve("./static/js");
const long_fn = path.join(base_path, short_fn);
if (module_mocks.has(long_fn)) {
throw new Error(`You already set up a mock for ${long_fn}`);
}
obj.__esModule = true;
module_mocks.set(long_fn, obj);
return obj;
};
2016-07-30 17:00:12 +02:00
exports.set_global = function (name, val) {
if (val === null) {
throw new Error(`
We try to avoid using null in our codebase.
`);
}
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;
};
exports.zrequire = function (fn) {
objs_installed = true;
const full_path = path.resolve(path.join("static/js", fn));
return require(full_path);
};
const staticPath = path.resolve(__dirname, "../../static") + path.sep;
const templatesPath = staticPath + "templates" + path.sep;
exports.finish = function () {
/*
Handle cleanup tasks after we've run one module.
Note that we currently do lazy compilation of modules,
so we need to wait till the module tests finish
running to do things like detecting pointless mocks
and resetting our _load hook.
*/
if (actual_load === undefined) {
throw new Error("namespace.finish was called without namespace.start.");
}
Module._load = actual_load;
actual_load = undefined;
for (const fn of module_mocks.keys()) {
if (!mocked_paths.has(fn)) {
throw new Error(`
You asked to mock ${fn} but we never
saw it during compilation.
`);
}
}
for (const path of Object.keys(require.cache)) {
if (path.startsWith(staticPath) && !path.startsWith(templatesPath)) {
delete require.cache[path];
}
}
Object.assign(global, old_globals);
old_globals = {};
for (const name of new_globals) {
delete global[name];
}
new_globals.clear();
2016-07-30 17:00:12 +02:00
};
exports.with_field = function (obj, field, val, f) {
if ("__esModule" in obj && "__Rewire__" in obj) {
const old_val = field in obj ? obj[field] : obj.__GetDependency__(field);
try {
obj.__Rewire__(field, val);
return f();
} finally {
obj.__Rewire__(field, old_val);
}
} else {
const had_val = Object.prototype.hasOwnProperty.call(obj, field);
const old_val = obj[field];
try {
obj[field] = val;
return f();
} finally {
if (had_val) {
obj[field] = old_val;
} else {
delete obj[field];
}
}
}
};
exports.with_overrides = function (test_function) {
// This function calls test_function() and passes in
// a way to override the namespace temporarily.
const restore_callbacks = [];
const unused_funcs = new Map();
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`.
if (typeof f !== "function") {
throw new TypeError(
"You can only override with a function. Use with_field for non-functions.",
);
}
if (typeof obj !== "object" && typeof obj !== "function") {
throw new TypeError(`We cannot override a function for ${typeof obj} objects`);
}
if (obj[func_name] !== undefined && typeof obj[func_name] !== "function") {
throw new TypeError(`
You are overriding a non-function with a function.
This is almost certainly an error.
`);
}
if (!unused_funcs.has(obj)) {
unused_funcs.set(obj, new Map());
}
unused_funcs.get(obj).set(func_name, true);
let old_f =
"__esModule" in obj && "__Rewire__" in obj && !(func_name in obj)
? obj.__GetDependency__(func_name)
: 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) {
unused_funcs.get(obj).delete(func_name);
return f.apply(this, args);
};
// 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;
if ("__esModule" in obj && "__Rewire__" in obj) {
obj.__Rewire__(func_name, new_f);
restore_callbacks.push(() => {
obj.__Rewire__(func_name, old_f);
});
} else {
obj[func_name] = new_f;
restore_callbacks.push(() => {
old_f._patched_with_override = true;
obj[func_name] = old_f;
delete old_f._patched_with_override;
});
}
};
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
}
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!");
}
}
};