Skip to content

Commit

Permalink
Merge pull request #545 from GuillaumeGomez/add-include
Browse files Browse the repository at this point in the history
Add `include` command
  • Loading branch information
GuillaumeGomez authored Oct 4, 2023
2 parents 6add1e9 + 47956af commit 306bd95
Show file tree
Hide file tree
Showing 63 changed files with 443 additions and 212 deletions.
11 changes: 11 additions & 0 deletions goml-script.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ Here's the command list:
* [`go-to`](#go-to)
* [`history-go-back`](#history-go-back)
* [`history-go-forward`](#history-go-forward)
* [`include`](#include)
* [`javascript`](#javascript)
* [`move-cursor-to`](#move-cursor-to)
* [`pause-on-error`](#pause-on-error)
Expand Down Expand Up @@ -1295,6 +1296,16 @@ history-go-back: 0 // disable timeout, be careful when using it!

Please note that if no `timeout` is specified, the one from the [`timeout`](#timeout) command is used.

#### include

**include** command loads the given path then runs it. If the path is not absolute, it'll be relative to the current file. The file is not loaded until the `include` command is run. Example:

```
// current file: /bar/foo.goml
include: "bar.goml" // It will load `/bar/bar.goml`
include: "/babar/foo.goml" // It will load `/babar/foo.goml`
```

#### javascript

**javascript** command enables/disables the javascript. If you want to render the page without javascript, don't forget to disable it before the call to `goto`! Examples:
Expand Down
44 changes: 35 additions & 9 deletions src/ast.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ const {
VariableElement,
} = require('./parser.js');
const utils = require('./utils');
const path = require('path');

const savedAsts = new Map();

function replaceVariable(elem, variables, functionArgs, forceVariableAsString, errors) {
const makeError = (message, line) => {
Expand Down Expand Up @@ -92,34 +95,33 @@ class CommandNode {
this.argsStart = 0;
this.argsEnd = 0;
}
this.errors = [];
this.text = text;
}

getInferredAst(variables, functionArgs) {
// We clone the AST to not modify the original. And because it's JS, it's super annoying
// to do...
let inferred = [];
const errors = [];
if (!this.hasVariable) {
inferred = this.ast.map(e => e.clone());
} else {
for (const elem of this.ast) {
const e = replaceVariables(
elem.clone(), variables, functionArgs, false, this.errors);
const e = replaceVariables(elem.clone(), variables, functionArgs, false, errors);
if (e !== null) {
inferred.push(e);
}
}
}
if (this.errors.length === 0) {
if (errors.length === 0) {
const validation = new ExpressionsValidator(inferred, true, this.text);
if (validation.errors.length !== 0) {
this.errors.push(...validation.errors);
errors.push(...validation.errors);
}
}
return {
'ast': inferred,
'errors': this.errors,
'errors': errors,
};
}

Expand Down Expand Up @@ -147,14 +149,31 @@ class CommandNode {
this.commandStart,
this.text,
);
n.errors.push(...this.errors);
return n;
}
}

class AstLoader {
constructor(filePath, content = null) {
this.text = content === null ? utils.readFile(filePath) : content;
constructor(filePath, currentDir, content = null) {
this.filePath = filePath;
this.absolutePath = null;
if (content === null) {
if (currentDir === null || path.isAbsolute(filePath)) {
this.absolutePath = path.normalize(filePath);
} else {
this.absolutePath = path.normalize(path.resolve(currentDir, filePath));
}
if (savedAsts.has(this.absolutePath)) {
const ast = savedAsts.get(this.absolutePath);
this.commands = ast.commands;
this.text = ast.text;
this.errors = ast.errors;
return;
}
this.text = utils.readFile(this.absolutePath);
} else {
this.text = content;
}
const parser = new Parser(this.text);
this.commands = [];
this.errors = [];
Expand Down Expand Up @@ -183,6 +202,13 @@ class AstLoader {
));
}
}
if (this.absolutePath !== null) {
savedAsts.set(this.absolutePath, {
'commands': this.commands,
'text': this.text,
'errors': this.errors,
});
}
}

hasErrors() {
Expand Down
84 changes: 69 additions & 15 deletions src/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ const { AstLoader } = require('./ast.js');
const process = require('process');
const commands = require('./commands/all.js');
const consts = require('./consts.js');
const path = require('path');
const { stripCommonPathsPrefix } = require('./utils.js');

const ORDERS = {
'assert': commands.parseAssert,
Expand Down Expand Up @@ -59,6 +61,7 @@ const ORDERS = {
'go-to': commands.parseGoTo,
'history-go-back': commands.parseHistoryGoBack,
'history-go-forward': commands.parseHistoryGoForward,
'include': commands.parseInclude,
'javascript': commands.parseJavascript,
'move-cursor-to': commands.parseMoveCursorTo,
'pause-on-error': commands.parsePauseOnError,
Expand Down Expand Up @@ -114,6 +117,7 @@ const FATAL_ERROR_COMMANDS = [
'emulate',
'focus',
'go-to',
'include',
'move-cursor-to',
'screenshot',
'scroll-to',
Expand Down Expand Up @@ -152,6 +156,7 @@ const NO_INTERACTION_COMMANDS = [
'expect-failure',
'fail-on-js-error',
'fail-on-request-error',
'include',
'javascript',
'screenshot-comparison',
'screenshot-on-failure',
Expand All @@ -166,23 +171,28 @@ const BEFORE_GOTO = [

class ParserWithContext {
constructor(filePath, options, content = null) {
this.ast = new AstLoader(filePath, content);
const ast = new AstLoader(filePath, null, content);
this.firstGotoParsed = false;
this.options = options;
this.callingFunc = [];
this.variables = options.variables;
this.definedFunctions = Object.create(null);
this.contexts = [
{
'commands': this.ast.commands,
'ast': ast,
'commands': ast.commands,
'currentCommand': 0,
'functionArgs': Object.create(null),
},
];
}

get_parser_errors() {
return this.ast.errors;
const context = this.get_current_context();
if (context === null) {
return null;
}
return context.ast.errors;
}

get_current_context() {
Expand Down Expand Up @@ -212,10 +222,21 @@ class ParserWithContext {
const text = [`${command.line}`];
for (let i = this.contexts.length - 2; i >= 0; --i) {
const c = this.contexts[i];
text.push(`from line ${c.commands[c.currentCommand].line}`);
const shortPath = stripCommonPathsPrefix(c.ast.absolutePath);
text.push(` from \`${shortPath}\` line ${c.commands[c.currentCommand].line}`);
}
return text.join('\n');
}

pushNewContext(context) {
this.contexts.push(context);
if (this.contexts.length > 100) {
return {
'error': 'reached maximum recursion size (100)',
'line': this.get_current_command_line(),
'fatal_error': true,
};
}
// return text.join(' \n');
return text.join(' ');
}

setup_user_function_call(ast) {
Expand Down Expand Up @@ -246,29 +267,61 @@ class ParserWithContext {
args[arg_name] = ret['args'][index].value;
}
}
this.contexts.push({
const context = this.get_current_context();
this.pushNewContext({
'ast': context.ast,
'commands': func.commands,
'currentCommand': 0,
'functionArgs': Object.assign({}, this.get_current_context().functionArgs, args),
'functionArgs': Object.assign({}, context.functionArgs, args),
});
if (this.contexts.length > 100) {
return {
'error': 'reached maximum stack size (100)',
'line': this.get_current_command_line(),
'fatal_error': true,
};
// We disable the `increasePos` in the context to prevent it to be done twice.
return this.get_next_command(false);
}

setup_include() {
const ret = commands.parseInclude(this);
if (ret.error !== undefined) {
ret.line = this.get_current_command_line();
if (ast.length !== 0) {
ret.error += ` (from command \`${ast[0].getErrorText()}\`)`;
}
return ret;
}
const dirPath = path.dirname(this.get_current_context().ast.absolutePath);
const ast = new AstLoader(ret.path, dirPath);
if (ast.hasErrors()) {
return {'errors': ast.errors};
}
this.pushNewContext({
'ast': ast,
'commands': ast.commands,
'currentCommand': 0,
'functionArgs': Object.create(null),
});
// We disable the `increasePos` in the context to prevent it to be done twice.
return this.get_next_command(false);
}

getCurrentFile() {
const context = this.get_current_context();
if (context === null) {
return '';
}
return context.ast.absolutePath;
}

run_order(order, ast) {
// This is needed because for now, all commands get access to the ast
// through `ParserWithContext`.
this.elems = ast;

if (order === 'call-function') {
// We need to special-case `call-function` since it needs to be interpreted when called.
// We need to special-case `call-function` since it needs to access variables of this
// class.
return this.setup_user_function_call(ast);
} else if (order === 'include') {
// We need to special-case `include` since it needs to parse a new file when called.
return this.setup_include();
} else if (!Object.prototype.hasOwnProperty.call(ORDERS, order)) {
return {'error': `Unknown command "${order}"`, 'line': this.get_current_command_line()};
}
Expand Down Expand Up @@ -326,6 +379,7 @@ class ParserWithContext {
const inferred = command.getInferredAst(this.variables, context.functionArgs);
if (inferred.errors.length !== 0) {
return {
'filePath': context.ast.absolutePath,
'line': command.line,
'errors': inferred.errors,
};
Expand Down
1 change: 1 addition & 0 deletions src/commands/all.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ module.exports = {
'parseGoTo': navigation.parseGoTo,
'parseHistoryGoBack': navigation.parseHistoryGoBack,
'parseHistoryGoForward': navigation.parseHistoryGoForward,
'parseInclude': general.parseInclude,
'parseJavascript': emulation.parseJavascript,
'parseMoveCursorTo': input.parseMoveCursorTo,
'parsePauseOnError': context_setters.parsePauseOnError,
Expand Down
21 changes: 21 additions & 0 deletions src/commands/general.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,10 +197,31 @@ function parseSetWindowProperty(parser) {
return parseObjPropertyInner(parser, 'window');
}

// Possible inputs:
//
// * "file path"
function parseInclude(parser) {
const elems = parser.elems;

if (elems.length === 0) {
return {'error': 'expected a file path, found nothing'};
} else if (elems.length !== 1) {
return {'error': `expected a file path, found \`${parser.getRawArgs()}\``};
} else if (elems[0].kind !== 'string') {
return {
'error': `expected a JSON dict, found \`${elems[0].getErrorText()}\` \
(${elems[0].getArticleKind()})`,
};
}

return {'path': elems[0].value };
}

module.exports = {
'parseSetLocalStorage': parseSetLocalStorage,
'parseScreenshot': parseScreenshot,
'innerParseScreenshot': innerParseScreenshot,
'parseSetDocumentProperty': parseSetDocumentProperty,
'parseSetWindowProperty': parseSetWindowProperty,
'parseInclude': parseInclude,
};
Loading

0 comments on commit 306bd95

Please sign in to comment.