zulip/frontend_tests/zjsunit/namespace.js

190 lines
5.8 KiB
JavaScript
Raw Normal View History

"use strict";
const path = require("path");
const new_globals = new Set();
let old_globals = {};
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.
`);
}
// 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;
}
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;
};
function require_path(name, fn) {
if (fn === undefined) {
fn = "../../static/js/" + name;
} else if (/^generated\/|^js\/|^shared\/|^third\//.test(fn)) {
// FIXME: Stealing part of the NPM namespace is confusing.
fn = "../../static/" + fn;
}
return fn;
}
exports.zrequire = function (name, fn) {
return require(require_path(name, fn));
};
exports.reset_module = function (name, fn) {
fn = require_path(name, fn);
delete require.cache[require.resolve(fn)];
return require(fn);
};
const staticPath = path.resolve(__dirname, "../../static") + path.sep;
const templatesPath = staticPath + "templates" + path.sep;
2016-07-30 17:00:12 +02:00
exports.restore = function () {
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) {
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 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) {
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.
`);
}
if (!funcs.has(obj)) {
funcs.set(obj, new Map());
}
if (funcs.get(obj).has(func_name)) {
// 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.
throw new Error(
"You can only override a function one time. Use with_field for more granular control.",
);
}
funcs.get(obj).set(func_name, true);
if (!unused_funcs.has(obj)) {
unused_funcs.set(obj, new Map());
}
unused_funcs.get(obj).set(func_name, true);
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) {
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;
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!");
}
}
};