diff --git a/README.md b/README.md new file mode 100755 index 0000000..06cb349 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# unitfile-parser diff --git a/package-lock.json b/package-lock.json old mode 100644 new mode 100755 diff --git a/package.json b/package.json old mode 100644 new mode 100755 index 474aa69..1d1cca6 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "src/mod.mjs", "type": "module", "scripts": { - "test": "node ./test/spec.mjs './test/examples/*.mjs'" + "test": "./scripts/run-tests" }, "author": "", "license": "ISC", diff --git a/scripts/run-tests b/scripts/run-tests new file mode 100755 index 0000000..0b9f877 --- /dev/null +++ b/scripts/run-tests @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -e + +node ./src/spec/string-to-ast.mjs; +node ./src/spec/ast-to-data.mjs; +node ./src/spec/data-to-ast.mjs; +node ./src/spec/ast-to-string.mjs; diff --git a/src/Lexer.mjs b/src/Lexer.mjs old mode 100644 new mode 100755 index 44c6f03..dd6523e --- a/src/Lexer.mjs +++ b/src/Lexer.mjs @@ -1,25 +1,23 @@ import chevrotain from "chevrotain"; const { createToken, Lexer } = chevrotain; -function getValueParser(...stoppers) { - return function exec(text, startOffset) { - for (let i = startOffset; i < text.length; i += 1) { - if (text[i] === '\\') { - i += 1; - continue; - } - - if (stoppers.includes(text[i])) { - if (i !== startOffset) { - return [text.substring(startOffset, i)]; - } else { - return null; - } - } +function valueParser(text, startOffset) { + for (let i = startOffset; i < text.length; i += 1) { + if (text[i] === '\\') { + i += 1; + continue; } - return [text.substr(startOffset)]; + if (['\n', '#'].includes(text[i])) { + if (i !== startOffset) { + return [text.substring(startOffset, i)]; + } else { + return null; + } + } } + + return [text.substr(startOffset)]; } export const SectionHeading = createToken({ @@ -33,53 +31,44 @@ export const Property = createToken({ }); export const Value = createToken({ name: "Value", - pattern: { exec: getValueParser('\n', '#') }, + pattern: { exec: valueParser }, line_breaks: true, pop_mode: true, }); -export const CommentStartNewline = createToken({ - name: "CommentStartNewline", - pattern: '\n#', - push_mode: "comment_mode", -}); -export const CommentStart = createToken({ - name: "CommentStart", - pattern: '#', - push_mode: "comment_mode", -}); export const Comment = createToken({ name: "Comment", - pattern: { exec: getValueParser('\n') }, - line_breaks: true, - pop_mode: true, + pattern: /#.*/, + line_breaks: false, }); export const WhiteSpace = createToken({ name: "WhiteSpace", pattern: /\s+/, group: Lexer.SKIPPED, }); +export const Newline = createToken({ + name: "Newline", + pattern: /\n+/, +}); export const tokens = [ - CommentStart, - CommentStartNewline, Value, Comment, SectionHeading, Property, WhiteSpace, + Newline, ]; export default new Lexer({ defaultMode: "line_mode", modes: { line_mode: [ - CommentStartNewline, - CommentStart, + Comment, SectionHeading, Property, + Newline, WhiteSpace, ], value_mode: [ Value ], - comment_mode: [ Comment ], }, }); diff --git a/src/Parser.mjs b/src/Parser.mjs old mode 100644 new mode 100755 index fb9b47e..59e7e50 --- a/src/Parser.mjs +++ b/src/Parser.mjs @@ -3,10 +3,9 @@ const { CstParser } = chevrotain; import { tokens, Comment, - CommentStart, - CommentStartNewline, Property, SectionHeading, + Newline, Value, } from "./Lexer.mjs"; @@ -15,25 +14,22 @@ export default class UnitFileParser extends CstParser { super(tokens); const $ = this; - $.RULE("commentLine", () => { - $.CONSUME(CommentStartNewline); - $.CONSUME(Comment); + $.RULE("value", () => { + $.CONSUME(Value); }); $.RULE("comment", () => { - $.CONSUME(CommentStart); $.CONSUME(Comment); }); - $.RULE("value", () => { - $.CONSUME(Value); - }); - $.RULE("sectionHeadingStatement", () => { $.CONSUME(SectionHeading); $.OPTION(() => { $.SUBRULE($.comment); }); + $.OPTION1(() => { + $.CONSUME(Newline); + }); }); $.RULE("propertyStatement", () => { @@ -47,7 +43,7 @@ export default class UnitFileParser extends CstParser { $.RULE("sectionStatement", () => { $.OR([ { ALT: () => $.SUBRULE($.propertyStatement) }, - { ALT: () => $.SUBRULE($.commentLine) }, + { ALT: () => $.SUBRULE($.comment) }, ]); }); @@ -55,6 +51,9 @@ export default class UnitFileParser extends CstParser { $.SUBRULE($.sectionHeadingStatement); $.MANY(() => { $.SUBRULE($.sectionStatement); + $.OPTION(() => { + $.CONSUME(Newline); + }); }); }); @@ -63,6 +62,7 @@ export default class UnitFileParser extends CstParser { $.OR([ { ALT: () => $.SUBRULE($.section) }, { ALT: () => $.SUBRULE($.comment) }, + { ALT: () => $.CONSUME(Newline) }, ]); }); }); diff --git a/src/ast-to-data.mjs b/src/ast-to-data.mjs new file mode 100755 index 0000000..eda4af0 --- /dev/null +++ b/src/ast-to-data.mjs @@ -0,0 +1,37 @@ +function nodeToData(node) { + switch (node.type) { + case "unitFile": + return node.sections.map(nodeToData); + case "section": + return { + title: node.title, + settings: node.body + .filter(n => n.type === "setting") + .reduce( + (acc, setting) => { + if (acc[setting.name]) { + return { + ...acc, + [setting.name]: [acc[setting.name], setting.value].flat(), + }; + } + + return { + ...acc, + [setting.name]: nodeToData(setting), + }; + }, + {}, + ), + }; + case "setting": + return node.value; + default: + throw new Error(`Unrecognized node type: ${node.type}`); + break; + } +} + +export default function astToData(file) { + return nodeToData(file); +} diff --git a/src/ast-to-string.mjs b/src/ast-to-string.mjs new file mode 100755 index 0000000..4abcd61 --- /dev/null +++ b/src/ast-to-string.mjs @@ -0,0 +1,23 @@ +function nodeToString(node) { + switch (node.type) { + case "unitFile": + return `${(node.comments || []).map(nodeToString).join('\n')} +${(node.sections || []).map(nodeToString).join('\n')}`; + case "comment": + return `# ${comment.value}`; + case "section": + const titleComment = ` ${node.titleComment ? nodeToString(node.titleComment) : ''}`; + return `[${node.title}]${titleComment} + ${(node.body || []).map(nodeToString).join('\n')}`; + case "setting": + const comment = ` ${node.comment ? nodeToString(node.comment) : ''}`; + return `${node.name}=${node.value}${comment}`; + default: + throw new Error(`Unrecognized node type: ${node.type}`); + break; + } +} + +export default function astToString(file) { + return nodeToString(file); +} diff --git a/src/data-to-ast.mjs b/src/data-to-ast.mjs new file mode 100755 index 0000000..fb46310 --- /dev/null +++ b/src/data-to-ast.mjs @@ -0,0 +1,40 @@ +function sectionToAst(section) { + return { + type: "section", + title: section.title, + body: Object.keys(section.settings) + .reduce( + (acc, key) => { + const val = section.settings[key]; + if (Array.isArray(val)) { + return [ + ...acc, + ...val.map(value => ({ + type: "setting", + name: key, + value, + })), + ]; + } + + return [ + ...acc, + { + type: "setting", + name: key, + value: val, + }, + ]; + }, + [], + ), + }; +} + +export default function dataToAst(file) { + return { + type: "unitFile", + comments: [], + sections: file.map(sectionToAst), + }; +} diff --git a/src/data-to-string.mjs b/src/data-to-string.mjs new file mode 100755 index 0000000..22fcc85 --- /dev/null +++ b/src/data-to-string.mjs @@ -0,0 +1,6 @@ +import astToString from "./ast-to-string.mjs"; +import dataToAst from "./data-to-ast.mjs"; + +export default function dataToString(data) { + return astToString(dataToAst(data)); +} diff --git a/src/mod.mjs b/src/mod.mjs old mode 100644 new mode 100755 index c862bae..bf12606 --- a/src/mod.mjs +++ b/src/mod.mjs @@ -1,30 +1,11 @@ export { default as Lexer } from "./Lexer.mjs"; export { default as Parser } from "./Parser.mjs"; -export { default as visitorCreator } from "./visitor.mjs"; +export { default as visitorCreator } from "./visitor-creator.mjs"; -import Parser from "./Parser.mjs"; -import Lexer from "./Lexer.mjs"; -import visitorCreator from "./visitor.mjs"; - -export default (input) => { - const parser = new Parser([], { outputCst: true }); - const lexingresult = Lexer.tokenize(input); - - if (lexingresult.errors.length > 0) { - console.dir(lexingresult, { depth: Infinity }); - throw new Error("Lexing errors detected") - } - - parser.input = lexingresult.tokens; - const cst = parser.unitFile(); - - if (parser.errors.length > 0) { - console.dir(parser.errors, { depth: Infinity }); - throw new Error("Parsing errors detected") - } - - const Visitor = visitorCreator(parser); - const visitor = new Visitor(); - const ast = visitor.visit(cst); - return ast; -} +export { default as stringToAst } from "./string-to-ast.mjs"; +export { default as stringToData } from "./string-to-data.mjs"; +export { default as astToData } from "./ast-to-data.mjs"; +export { default as astToString } from "./ast-to-string.mjs"; +export { default as astToSTring } from "./ast-to-string.mjs"; +export { default as dataToAst } from "./data-to-ast.mjs"; +export { default as dataToString } from "./data-to-string.mjs"; diff --git a/src/spec/ast-to-data.mjs b/src/spec/ast-to-data.mjs new file mode 100755 index 0000000..5ee2b84 --- /dev/null +++ b/src/spec/ast-to-data.mjs @@ -0,0 +1,44 @@ +import astToData from "../ast-to-data.mjs"; + +function runTest(input, expectedOutput) { + const result = astToData(input); + + const expectedOutputString = JSON.stringify(expectedOutput); + const resultString = JSON.stringify(result); + if (expectedOutputString !== resultString) { + console.dir(input, { depth: Infinity }); + throw new Error(`Mismatching result on testcase: +=======Result====== + +${resultString} + +=======Expected====== + +${expectedOutputString}\n\n`); + } +} + + +(async () => { + const cases = await Promise.all([ + "./parser/basic.mjs", + "./parser/comments.mjs", + "./parser/comment-only.mjs", + "./parser/multiline-value.mjs", + "./parser/weird-characters.mjs", + "./parser/repeated-settings.mjs", + "./parser/real.nix-daemon.mjs", + "./parser/real.maia-console@.mjs", + /* + "./parser/real.dbus-org.bluez.mjs", + "./parser/real.display-manager.mjs", + "./parser/real.systemd-fsck-silent@.mjs", + "./parser/real.systemd-fsck-silent-root.mjs", + "./parser/real.dbus-org.freedesktop.Avahi.mjs", + "./parser/real.dbus-org.freedesktop.ModemManager1.mjs", + "./parser/real.dbus-org.freedesktop.nm-dispatcher.mjs", + */ + ].map(file => import(file))); + const results = cases.forEach(({ ast, data }) => runTest(ast, data)); +})(); + diff --git a/test/examples/basic.mjs b/src/spec/ast-to-data/basic.mjs old mode 100644 new mode 100755 similarity index 66% rename from test/examples/basic.mjs rename to src/spec/ast-to-data/basic.mjs index 5c59662..5bc459c --- a/test/examples/basic.mjs +++ b/src/spec/ast-to-data/basic.mjs @@ -1,7 +1,4 @@ -export const content = `[Unit] -Description=Idle manager for Wayland`; - -export const result = { +export const input = { type: 'unitFile', comments: [], sections: [ @@ -19,3 +16,12 @@ export const result = { } ] }; + +export const output = [ + { + title: 'Unit', + settings: { + Description: 'Idle manager for Wayland', + }, + } +]; diff --git a/src/spec/ast-to-data/full-tree.mjs b/src/spec/ast-to-data/full-tree.mjs new file mode 100755 index 0000000..28fd0f0 --- /dev/null +++ b/src/spec/ast-to-data/full-tree.mjs @@ -0,0 +1,2 @@ +export const input = ; +export const result = ; diff --git a/src/spec/ast-to-string.mjs b/src/spec/ast-to-string.mjs new file mode 100755 index 0000000..e69de29 diff --git a/src/spec/data-to-ast.mjs b/src/spec/data-to-ast.mjs new file mode 100755 index 0000000..444ed2f --- /dev/null +++ b/src/spec/data-to-ast.mjs @@ -0,0 +1,44 @@ +import astToData from "../data-to-ast.mjs"; + +function runTest(input, expectedOutput) { + const result = astToData(input); + + const expectedOutputString = JSON.stringify(expectedOutput); + const resultString = JSON.stringify(result); + if (expectedOutputString !== resultString) { + console.dir(input, { depth: Infinity }); + throw new Error(`Mismatching result on testcase: +=======Result====== + +${resultString} + +=======Expected====== + +${expectedOutputString}\n\n`); + } +} + + +(async () => { + const cases = await Promise.all([ + "./parser/basic.mjs", + "./parser/comments.mjs", + "./parser/comment-only.mjs", + "./parser/multiline-value.mjs", + "./parser/weird-characters.mjs", + "./parser/repeated-settings.mjs", + "./parser/real.nix-daemon.mjs", + "./parser/real.maia-console@.mjs", + /* + "./parser/real.dbus-org.bluez.mjs", + "./parser/real.display-manager.mjs", + "./parser/real.systemd-fsck-silent@.mjs", + "./parser/real.systemd-fsck-silent-root.mjs", + "./parser/real.dbus-org.freedesktop.Avahi.mjs", + "./parser/real.dbus-org.freedesktop.ModemManager1.mjs", + "./parser/real.dbus-org.freedesktop.nm-dispatcher.mjs", + */ + ].map(file => import(file))); + const results = cases.forEach(({ ast, data }) => runTest(ast, data)); +})(); + diff --git a/src/spec/data-to-string.mjs b/src/spec/data-to-string.mjs new file mode 100755 index 0000000..e69de29 diff --git a/src/spec/parser/basic.mjs b/src/spec/parser/basic.mjs new file mode 100755 index 0000000..d3b102a --- /dev/null +++ b/src/spec/parser/basic.mjs @@ -0,0 +1,30 @@ +export const string = `[Unit] +Description=Idle manager for Wayland`; + +export const ast = { + type: 'unitFile', + comments: [], + sections: [ + { + type: 'section', + title: 'Unit', + titleComment: undefined, + body: [ + { + type: 'setting', + name: 'Description', + value: 'Idle manager for Wayland' + } + ] + } + ] +}; + +export const data = [ + { + title: 'Unit', + settings: { + Description: 'Idle manager for Wayland', + }, + } +]; diff --git a/src/spec/parser/comment-only.mjs b/src/spec/parser/comment-only.mjs new file mode 100755 index 0000000..c9e2497 --- /dev/null +++ b/src/spec/parser/comment-only.mjs @@ -0,0 +1,9 @@ +export const string = `# Start of file comment`; + +export const ast = { + type: 'unitFile', + comments: [ { type: 'comment', value: 'Start of file comment' } ], + sections: [], +}; + +export const data = []; diff --git a/test/examples/comments.mjs b/src/spec/parser/comments.mjs old mode 100644 new mode 100755 similarity index 72% rename from test/examples/comments.mjs rename to src/spec/parser/comments.mjs index 5299a96..402e161 --- a/test/examples/comments.mjs +++ b/src/spec/parser/comments.mjs @@ -1,4 +1,5 @@ -export const content = `# Outside comment +export const string = `# Outside comment +# Outside comment part 2 [Unit] # Heading comment Description=Idle manager for Wayland # End of value comment # Inline comment @@ -10,9 +11,12 @@ Alias=asdf#Comment Without spaces # Comment only in this body `; -export const result = { +export const ast = { type: 'unitFile', - comments: [ { type: 'comment', value: 'Outside comment' } ], + comments: [ + { type: 'comment', value: 'Outside comment' }, + { type: 'comment', value: 'Outside comment part 2' }, + ], sections: [ { type: 'section', @@ -50,3 +54,18 @@ export const result = { } ] }; + +export const data = [ + { + title: 'Unit', + settings: { + Description: 'Idle manager for Wayland', + ExecStart: 'echo "some string"', + Alias: 'asdf', + }, + }, + { + title: 'Install', + settings: {}, + } +]; diff --git a/test/examples/multiline-value.mjs b/src/spec/parser/multiline-value.mjs old mode 100644 new mode 100755 similarity index 70% rename from test/examples/multiline-value.mjs rename to src/spec/parser/multiline-value.mjs index 51331a3..fbe68e7 --- a/test/examples/multiline-value.mjs +++ b/src/spec/parser/multiline-value.mjs @@ -1,4 +1,4 @@ -export const content = ` +export const string = ` [Service] ExecStart=/usr/bin/swayidle -w \\ timeout 600 'swaylock-bg' \\ @@ -11,7 +11,7 @@ ExecPause=/usr/bin/swayidle -w \\ after-resume 'swaylock-bg'#No space comment `; -export const result = { +export const ast = { type: 'unitFile', comments: [], sections: [ @@ -46,3 +46,18 @@ export const result = { } ] }; + +export const data = [ + { + title: 'Service', + settings: { + ExecStart: '/usr/bin/swayidle -w \\\n' + + " timeout 600 'swaylock-bg' \\\n" + + ` timeout 900 'swaymsg "output * dpms off"' \\\n` + + ` resume 'swaymsg "output * dpms on"' \\\n` + + " after-resume 'swaylock-bg'", + ExecStop: "/usr/bin/swayidle -w \\\n after-resume 'swaylock-bg'", + ExecPause: "/usr/bin/swayidle -w \\\n after-resume 'swaylock-bg'", + }, + }, +]; diff --git a/src/spec/parser/real.dbus-org.bluez.mjs b/src/spec/parser/real.dbus-org.bluez.mjs new file mode 100755 index 0000000..0dbb34e --- /dev/null +++ b/src/spec/parser/real.dbus-org.bluez.mjs @@ -0,0 +1,118 @@ +export const string = `[Unit] +Description=Bluetooth service +Documentation=man:bluetoothd(8) +ConditionPathIsDirectory=/sys/class/bluetooth + +[Service] +Type=dbus +BusName=org.bluez +ExecStart=/usr/lib/bluetooth/bluetoothd +NotifyAccess=main +#WatchdogSec=10 +#Restart=on-failure +CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE +LimitNPROC=1 +ProtectHome=true +ProtectSystem=full + +[Install] +WantedBy=bluetooth.target +Alias=dbus-org.bluez.service`; + +export const ast = { + "type": "unitFile", + "comments": [], + "sections": [ + { + "type": "section", + "title": "Unit", + "body": [ + { + "type": "setting", + "name": "Description", + "value": "Bluetooth service" + }, + { + "type": "setting", + "name": "Documentation", + "value": "man:bluetoothd(8)" + }, + { + "type": "setting", + "name": "ConditionPathIsDirectory", + "value": "/sys/class/bluetooth" + } + ] + }, + { + "type": "section", + "title": "Service", + "body": [ + { + "type": "setting", + "name": "Type", + "value": "dbus" + }, + { + "type": "setting", + "name": "BusName", + "value": "org.bluez" + }, + { + "type": "setting", + "name": "ExecStart", + "value": "/usr/lib/bluetooth/bluetoothd" + }, + { + "type": "setting", + "name": "NotifyAccess", + "value": "main" + }, + { + "type": "comment", + "value": "WatchdogSec=10" + }, + { + "type": "comment", + "value": "Restart=on-failure" + }, + { + "type": "setting", + "name": "CapabilityBoundingSet", + "value": "CAP_NET_ADMIN CAP_NET_BIND_SERVICE" + }, + { + "type": "setting", + "name": "LimitNPROC", + "value": "1" + }, + { + "type": "setting", + "name": "ProtectHome", + "value": "true" + }, + { + "type": "setting", + "name": "ProtectSystem", + "value": "full" + } + ] + }, + { + "type": "section", + "title": "Install", + "body": [ + { + "type": "setting", + "name": "WantedBy", + "value": "bluetooth.target" + }, + { + "type": "setting", + "name": "Alias", + "value": "dbus-org.bluez.service" + } + ] + } + ] +}; diff --git a/src/spec/parser/real.dbus-org.freedesktop.Avahi.mjs b/src/spec/parser/real.dbus-org.freedesktop.Avahi.mjs new file mode 100755 index 0000000..14f1fef --- /dev/null +++ b/src/spec/parser/real.dbus-org.freedesktop.Avahi.mjs @@ -0,0 +1,172 @@ +export const string = `# This file is part of avahi. +# +# avahi is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# avahi is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public +# License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with avahi; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA. + +[Unit] +Description=Avahi mDNS/DNS-SD Stack +Requires=avahi-daemon.socket + +[Service] +Type=dbus +BusName=org.freedesktop.Avahi +ExecStart=/usr/bin/avahi-daemon -s +ExecReload=/usr/bin/avahi-daemon -r +NotifyAccess=main + +[Install] +WantedBy=multi-user.target +Also=avahi-daemon.socket +Alias=dbus-org.freedesktop.Avahi.service` + +export const ast = { + "type": "unitFile", + "comments": [ + { + "type": "comment", + "value": "This file is part of avahi." + }, + { + "type": "comment", + "value": "" + }, + { + "type": "comment", + "value": "avahi is free software; you can redistribute it and/or modify it" + }, + { + "type": "comment", + "value": "under the terms of the GNU Lesser General Public License as" + }, + { + "type": "comment", + "value": "published by the Free Software Foundation; either version 2 of the" + }, + { + "type": "comment", + "value": "License, or (at your option) any later version." + }, + { + "type": "comment", + "value": "" + }, + { + "type": "comment", + "value": "avahi is distributed in the hope that it will be useful, but WITHOUT" + }, + { + "type": "comment", + "value": "ANY WARRANTY; without even the implied warranty of MERCHANTABILITY" + }, + { + "type": "comment", + "value": "or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public" + }, + { + "type": "comment", + "value": "License for more details." + }, + { + "type": "comment", + "value": "" + }, + { + "type": "comment", + "value": "You should have received a copy of the GNU Lesser General Public" + }, + { + "type": "comment", + "value": "License along with avahi; if not, write to the Free Software" + }, + { + "type": "comment", + "value": "Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307" + }, + { + "type": "comment", + "value": "USA." + } + ], + "sections": [ + { + "type": "section", + "title": "Unit", + "body": [ + { + "type": "setting", + "name": "Description", + "value": "Avahi mDNS/DNS-SD Stack" + }, + { + "type": "setting", + "name": "Requires", + "value": "avahi-daemon.socket" + } + ] + }, + { + "type": "section", + "title": "Service", + "body": [ + { + "type": "setting", + "name": "Type", + "value": "dbus" + }, + { + "type": "setting", + "name": "BusName", + "value": "org.freedesktop.Avahi" + }, + { + "type": "setting", + "name": "ExecStart", + "value": "/usr/bin/avahi-daemon -s" + }, + { + "type": "setting", + "name": "ExecReload", + "value": "/usr/bin/avahi-daemon -r" + }, + { + "type": "setting", + "name": "NotifyAccess", + "value": "main" + } + ] + }, + { + "type": "section", + "title": "Install", + "body": [ + { + "type": "setting", + "name": "WantedBy", + "value": "multi-user.target" + }, + { + "type": "setting", + "name": "Also", + "value": "avahi-daemon.socket" + }, + { + "type": "setting", + "name": "Alias", + "value": "dbus-org.freedesktop.Avahi.service" + } + ] + } + ] +}; diff --git a/src/spec/parser/real.dbus-org.freedesktop.ModemManager1.mjs b/src/spec/parser/real.dbus-org.freedesktop.ModemManager1.mjs new file mode 100755 index 0000000..7742fca --- /dev/null +++ b/src/spec/parser/real.dbus-org.freedesktop.ModemManager1.mjs @@ -0,0 +1,132 @@ +export const string = `[Unit] +Description=Modem Manager +After=polkit.service +Requires=polkit.service + +[Service] +Type=dbus +BusName=org.freedesktop.ModemManager1 +ExecStart=/usr/bin/ModemManager +StandardError=null +Restart=on-abort +CapabilityBoundingSet=CAP_SYS_ADMIN +ProtectSystem=true +ProtectHome=true +PrivateTmp=true +RestrictAddressFamilies=AF_NETLINK AF_UNIX +NoNewPrivileges=true +User=root + +[Install] +WantedBy=multi-user.target +Alias=dbus-org.freedesktop.ModemManager1.service`; + +export const ast = { + "type": "unitFile", + "comments": [], + "sections": [ + { + "type": "section", + "title": "Unit", + "body": [ + { + "type": "setting", + "name": "Description", + "value": "Modem Manager" + }, + { + "type": "setting", + "name": "After", + "value": "polkit.service" + }, + { + "type": "setting", + "name": "Requires", + "value": "polkit.service" + } + ] + }, + { + "type": "section", + "title": "Service", + "body": [ + { + "type": "setting", + "name": "Type", + "value": "dbus" + }, + { + "type": "setting", + "name": "BusName", + "value": "org.freedesktop.ModemManager1" + }, + { + "type": "setting", + "name": "ExecStart", + "value": "/usr/bin/ModemManager" + }, + { + "type": "setting", + "name": "StandardError", + "value": "null" + }, + { + "type": "setting", + "name": "Restart", + "value": "on-abort" + }, + { + "type": "setting", + "name": "CapabilityBoundingSet", + "value": "CAP_SYS_ADMIN" + }, + { + "type": "setting", + "name": "ProtectSystem", + "value": "true" + }, + { + "type": "setting", + "name": "ProtectHome", + "value": "true" + }, + { + "type": "setting", + "name": "PrivateTmp", + "value": "true" + }, + { + "type": "setting", + "name": "RestrictAddressFamilies", + "value": "AF_NETLINK AF_UNIX" + }, + { + "type": "setting", + "name": "NoNewPrivileges", + "value": "true" + }, + { + "type": "setting", + "name": "User", + "value": "root" + } + ] + }, + { + "type": "section", + "title": "Install", + "body": [ + { + "type": "setting", + "name": "WantedBy", + "value": "multi-user.target" + }, + { + "type": "setting", + "name": "Alias", + "value": "dbus-org.freedesktop.ModemManager1.service" + } + ] + } + ] +}; diff --git a/src/spec/parser/real.dbus-org.freedesktop.nm-dispatcher.mjs b/src/spec/parser/real.dbus-org.freedesktop.nm-dispatcher.mjs new file mode 100755 index 0000000..8f8be57 --- /dev/null +++ b/src/spec/parser/real.dbus-org.freedesktop.nm-dispatcher.mjs @@ -0,0 +1,78 @@ +export const string = `[Unit] +Description=Network Manager Script Dispatcher Service + +[Service] +Type=dbus +BusName=org.freedesktop.nm_dispatcher +ExecStart=/usr/lib/nm-dispatcher + +# We want to allow scripts to spawn long-running daemons, so tell +# systemd to not clean up when nm-dispatcher exits +KillMode=process + +[Install] +Alias=dbus-org.freedesktop.nm-dispatcher.service +`; + +export const ast = { + "type": "unitFile", + "comments": [], + "sections": [ + { + "type": "section", + "title": "Unit", + "body": [ + { + "type": "setting", + "name": "Description", + "value": "Network Manager Script Dispatcher Service" + } + ] + }, + { + "type": "section", + "title": "Service", + "body": [ + { + "type": "setting", + "name": "Type", + "value": "dbus" + }, + { + "type": "setting", + "name": "BusName", + "value": "org.freedesktop.nm_dispatcher" + }, + { + "type": "setting", + "name": "ExecStart", + "value": "/usr/lib/nm-dispatcher" + }, + { + "type": "comment", + "value": "We want to allow scripts to spawn long-running daemons, so tell" + }, + { + "type": "comment", + "value": "systemd to not clean up when nm-dispatcher exits" + }, + { + "type": "setting", + "name": "KillMode", + "value": "process" + } + ] + }, + { + "type": "section", + "title": "Install", + "body": [ + { + "type": "setting", + "name": "Alias", + "value": "dbus-org.freedesktop.nm-dispatcher.service" + } + ] + } + ] +}; diff --git a/src/spec/parser/real.display-manager.mjs b/src/spec/parser/real.display-manager.mjs new file mode 100755 index 0000000..c0c35e1 --- /dev/null +++ b/src/spec/parser/real.display-manager.mjs @@ -0,0 +1,90 @@ +export const string = `[Unit] +Description=TUI display manager +After=systemd-user-sessions.service plymouth-quit-wait.service +After=getty@tty2.service + +[Service] +Type=idle +ExecStart=/usr/bin/ly +StandardInput=tty +TTYPath=/dev/tty2 +TTYReset=yes +TTYVHangup=yes + +[Install] +Alias=display-manager.service`; + +export const ast = { + "type": "unitFile", + "comments": [], + "sections": [ + { + "type": "section", + "title": "Unit", + "body": [ + { + "type": "setting", + "name": "Description", + "value": "TUI display manager" + }, + { + "type": "setting", + "name": "After", + "value": "systemd-user-sessions.service plymouth-quit-wait.service" + }, + { + "type": "setting", + "name": "After", + "value": "getty@tty2.service" + } + ] + }, + { + "type": "section", + "title": "Service", + "body": [ + { + "type": "setting", + "name": "Type", + "value": "idle" + }, + { + "type": "setting", + "name": "ExecStart", + "value": "/usr/bin/ly" + }, + { + "type": "setting", + "name": "StandardInput", + "value": "tty" + }, + { + "type": "setting", + "name": "TTYPath", + "value": "/dev/tty2" + }, + { + "type": "setting", + "name": "TTYReset", + "value": "yes" + }, + { + "type": "setting", + "name": "TTYVHangup", + "value": "yes" + } + ] + }, + { + "type": "section", + "title": "Install", + "body": [ + { + "type": "setting", + "name": "Alias", + "value": "display-manager.service" + } + ] + } + ] +}; diff --git a/src/spec/parser/real.maia-console@.mjs b/src/spec/parser/real.maia-console@.mjs new file mode 100755 index 0000000..bae85eb --- /dev/null +++ b/src/spec/parser/real.maia-console@.mjs @@ -0,0 +1,104 @@ +export const string = `[Unit] +Description=maia color scheme for the console +After=getty@%i.service + +[Service] +Type=oneshot +ExecStart=/usr/bin/maia-console +StandardOutput=tty +TTYPath=/dev/%i +TTYVTDisallocate=yes + +[Install] +WantedBy=getty@%i.service`; + +export const ast = { + "type": "unitFile", + "comments": [], + "sections": [ + { + "type": "section", + "title": "Unit", + "body": [ + { + "type": "setting", + "name": "Description", + "value": "maia color scheme for the console" + }, + { + "type": "setting", + "name": "After", + "value": "getty@%i.service" + } + ] + }, + { + "type": "section", + "title": "Service", + "body": [ + { + "type": "setting", + "name": "Type", + "value": "oneshot" + }, + { + "type": "setting", + "name": "ExecStart", + "value": "/usr/bin/maia-console" + }, + { + "type": "setting", + "name": "StandardOutput", + "value": "tty" + }, + { + "type": "setting", + "name": "TTYPath", + "value": "/dev/%i" + }, + { + "type": "setting", + "name": "TTYVTDisallocate", + "value": "yes" + } + ] + }, + { + "type": "section", + "title": "Install", + "body": [ + { + "type": "setting", + "name": "WantedBy", + "value": "getty@%i.service" + } + ] + } + ] +}; + +export const data = [ + { + title: 'Unit', + settings: { + Description: 'maia color scheme for the console', + After: 'getty@%i.service', + }, + }, + { + title: 'Service', + settings: { + Type: 'oneshot', + ExecStart: '/usr/bin/maia-console', + StandardOutput: 'tty', + TTYPath: '/dev/%i', + TTYVTDisallocate: 'yes', + }, + }, + { + title: 'Install', + settings: { + WantedBy: 'getty@%i.service', + }, + }, +]; diff --git a/src/spec/parser/real.nix-daemon.mjs b/src/spec/parser/real.nix-daemon.mjs new file mode 100755 index 0000000..146bbda --- /dev/null +++ b/src/spec/parser/real.nix-daemon.mjs @@ -0,0 +1,99 @@ +export const string = `[Unit] +Description=Nix Daemon +RequiresMountsFor=/nix/store +RequiresMountsFor=/nix/var +ConditionPathIsReadWrite=/nix/var/nix/daemon-socket + +[Service] +ExecStart=@/nix/store/fwak7l5jjl0py4wldsqjbv7p7rdzql0b-nix-2.3.9/bin/nix-daemon nix-daemon --daemon +KillMode=process + +[Install] +WantedBy=multi-user.target`; + +export const ast = { + "type": "unitFile", + "comments": [], + "sections": [ + { + "type": "section", + "title": "Unit", + "body": [ + { + "type": "setting", + "name": "Description", + "value": "Nix Daemon" + }, + { + "type": "setting", + "name": "RequiresMountsFor", + "value": "/nix/store" + }, + { + "type": "setting", + "name": "RequiresMountsFor", + "value": "/nix/var" + }, + { + "type": "setting", + "name": "ConditionPathIsReadWrite", + "value": "/nix/var/nix/daemon-socket" + } + ] + }, + { + "type": "section", + "title": "Service", + "body": [ + { + "type": "setting", + "name": "ExecStart", + "value": "@/nix/store/fwak7l5jjl0py4wldsqjbv7p7rdzql0b-nix-2.3.9/bin/nix-daemon nix-daemon --daemon" + }, + { + "type": "setting", + "name": "KillMode", + "value": "process" + } + ] + }, + { + "type": "section", + "title": "Install", + "body": [ + { + "type": "setting", + "name": "WantedBy", + "value": "multi-user.target" + } + ] + } + ] +}; + +export const data = [ + { + title: 'Unit', + settings: { + Description: 'Nix Daemon', + RequiresMountsFor: [ + '/nix/store', + '/nix/var', + ], + ConditionPathIsReadWrite: '/nix/var/nix/daemon-socket', + }, + }, + { + title: 'Service', + settings: { + ExecStart: '@/nix/store/fwak7l5jjl0py4wldsqjbv7p7rdzql0b-nix-2.3.9/bin/nix-daemon nix-daemon --daemon', + KillMode: 'process', + }, + }, + { + title: 'Install', + settings: { + WantedBy: 'multi-user.target', + }, + }, +]; diff --git a/src/spec/parser/real.systemd-fsck-silent-root.mjs b/src/spec/parser/real.systemd-fsck-silent-root.mjs new file mode 100755 index 0000000..972ac3a --- /dev/null +++ b/src/spec/parser/real.systemd-fsck-silent-root.mjs @@ -0,0 +1,130 @@ +export const string = `# SPDX-License-Identifier: LGPL-2.1+ +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=File System Check on Root Device +Documentation=man:systemd-fsck-root.service(8) +DefaultDependencies=no +Before=local-fs.target shutdown.target +ConditionPathIsReadWrite=!/ + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/usr/lib/systemd/systemd-fsck +StandardOutput=null +StandardError=journal+console +TimeoutSec=0`; + +export const ast = { + "type": "unitFile", + "comments": [ + { + "type": "comment", + "value": " SPDX-License-Identifier: LGPL-2.1+" + }, + { + "type": "comment", + "value": "" + }, + { + "type": "comment", + "value": " This file is part of systemd." + }, + { + "type": "comment", + "value": "" + }, + { + "type": "comment", + "value": " systemd is free software; you can redistribute it and/or modify it" + }, + { + "type": "comment", + "value": " under the terms of the GNU Lesser General Public License as published by" + }, + { + "type": "comment", + "value": " the Free Software Foundation; either version 2.1 of the License, or" + }, + { + "type": "comment", + "value": " (at your option) any later version." + } + ], + "sections": [ + { + "type": "section", + "title": "Unit", + "body": [ + { + "type": "setting", + "name": "Description", + "value": "File System Check on Root Device" + }, + { + "type": "setting", + "name": "Documentation", + "value": "man:systemd-fsck-root.service(8)" + }, + { + "type": "setting", + "name": "DefaultDependencies", + "value": "no" + }, + { + "type": "setting", + "name": "Before", + "value": "local-fs.target shutdown.target" + }, + { + "type": "setting", + "name": "ConditionPathIsReadWrite", + "value": "!/" + } + ] + }, + { + "type": "section", + "title": "Service", + "body": [ + { + "type": "setting", + "name": "Type", + "value": "oneshot" + }, + { + "type": "setting", + "name": "RemainAfterExit", + "value": "yes" + }, + { + "type": "setting", + "name": "ExecStart", + "value": "/usr/lib/systemd/systemd-fsck" + }, + { + "type": "setting", + "name": "StandardOutput", + "value": "null" + }, + { + "type": "setting", + "name": "StandardError", + "value": "journal+console" + }, + { + "type": "setting", + "name": "TimeoutSec", + "value": "0" + } + ] + } + ] +}; diff --git a/src/spec/parser/real.systemd-fsck-silent@.mjs b/src/spec/parser/real.systemd-fsck-silent@.mjs new file mode 100755 index 0000000..7ea403a --- /dev/null +++ b/src/spec/parser/real.systemd-fsck-silent@.mjs @@ -0,0 +1,136 @@ +export const string = `# SPDX-License-Identifier: LGPL-2.1+ +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=File System Check on %f +Documentation=man:systemd-fsck@.service(8) +DefaultDependencies=no +BindsTo=%i.device +After=%i.device systemd-fsck-silent-root.service local-fs-pre.target +Before=systemd-quotacheck.service shutdown.target + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/usr/lib/systemd/systemd-fsck +StandardOutput=null +StandardError=journal+console +TimeoutSec=0`; + +export const ast = { + "type": "unitFile", + "comments": [ + { + "type": "comment", + "value": " SPDX-License-Identifier: LGPL-2.1+" + }, + { + "type": "comment", + "value": "" + }, + { + "type": "comment", + "value": " This file is part of systemd." + }, + { + "type": "comment", + "value": "" + }, + { + "type": "comment", + "value": " systemd is free software; you can redistribute it and/or modify it" + }, + { + "type": "comment", + "value": " under the terms of the GNU Lesser General Public License as published by" + }, + { + "type": "comment", + "value": " the Free Software Foundation; either version 2.1 of the License, or" + }, + { + "type": "comment", + "value": " (at your option) any later version." + } + ], + "sections": [ + { + "type": "section", + "title": "Unit", + "body": [ + { + "type": "setting", + "name": "Description", + "value": "File System Check on %f" + }, + { + "type": "setting", + "name": "Documentation", + "value": "man:systemd-fsck@.service(8)" + }, + { + "type": "setting", + "name": "DefaultDependencies", + "value": "no" + }, + { + "type": "setting", + "name": "BindsTo", + "value": "%i.device" + }, + { + "type": "setting", + "name": "After", + "value": "%i.device systemd-fsck-silent-root.service local-fs-pre.target" + }, + { + "type": "setting", + "name": "Before", + "value": "systemd-quotacheck.service shutdown.target" + } + ] + }, + { + "type": "section", + "title": "Service", + "body": [ + { + "type": "setting", + "name": "Type", + "value": "oneshot" + }, + { + "type": "setting", + "name": "RemainAfterExit", + "value": "yes" + }, + { + "type": "setting", + "name": "ExecStart", + "value": "/usr/lib/systemd/systemd-fsck" + }, + { + "type": "setting", + "name": "StandardOutput", + "value": "null" + }, + { + "type": "setting", + "name": "StandardError", + "value": "journal+console" + }, + { + "type": "setting", + "name": "TimeoutSec", + "value": "0" + } + ] + } + ] +}; diff --git a/test/examples/repeated-settings.mjs b/src/spec/parser/repeated-settings.mjs old mode 100644 new mode 100755 similarity index 72% rename from test/examples/repeated-settings.mjs rename to src/spec/parser/repeated-settings.mjs index 70575a7..38f5bb7 --- a/test/examples/repeated-settings.mjs +++ b/src/spec/parser/repeated-settings.mjs @@ -1,9 +1,9 @@ -export const content = `[Install] +export const string = `[Install] After=something.service After=something-else.service After=something-different.service`; -export const result = { +export const ast = { type: 'unitFile', comments: [], sections: [ @@ -34,3 +34,16 @@ export const result = { } ] }; + +export const data = [ + { + title: 'Install', + settings: { + After: [ + 'something.service', + 'something-else.service', + 'something-different.service', + ], + }, + } +]; diff --git a/test/examples/weird-characters.mjs b/src/spec/parser/weird-characters.mjs old mode 100644 new mode 100755 similarity index 68% rename from test/examples/weird-characters.mjs rename to src/spec/parser/weird-characters.mjs index ede9a4e..b734b2c --- a/test/examples/weird-characters.mjs +++ b/src/spec/parser/weird-characters.mjs @@ -1,7 +1,7 @@ -export const content = `[Install] +export const string = `[Install] Description=Test description(1) https://something/? \\# # test`; -export const result = { +export const ast = { type: 'unitFile', comments: [], sections: [ @@ -20,3 +20,12 @@ export const result = { } ] }; + +export const data = [ + { + title: 'Install', + settings: { + Description: 'Test description(1) https://something/? \\#', + }, + } +]; diff --git a/src/spec/string-to-ast.mjs b/src/spec/string-to-ast.mjs new file mode 100755 index 0000000..242e854 --- /dev/null +++ b/src/spec/string-to-ast.mjs @@ -0,0 +1,43 @@ +import stringToAst from "../string-to-ast.mjs"; + +function runTest(input, result) { + const ast = stringToAst(input); + + const resultString = JSON.stringify(ast, null, 2); + const expectedString = JSON.stringify(result, null, 2); + if (resultString !== expectedString) { + throw new Error(`mismatching result on testcase: +${input} + +=======result====== + +${resultString} + +=======expected====== + +${expectedString} + +`); + } +}; + +(async () => { + const cases = await Promise.all([ + "./parser/basic.mjs", + "./parser/comments.mjs", + "./parser/comment-only.mjs", + "./parser/multiline-value.mjs", + "./parser/weird-characters.mjs", + "./parser/repeated-settings.mjs", + "./parser/real.nix-daemon.mjs", + "./parser/real.maia-console@.mjs", + "./parser/real.dbus-org.bluez.mjs", + "./parser/real.display-manager.mjs", + "./parser/real.systemd-fsck-silent@.mjs", + "./parser/real.systemd-fsck-silent-root.mjs", + "./parser/real.dbus-org.freedesktop.Avahi.mjs", + "./parser/real.dbus-org.freedesktop.ModemManager1.mjs", + "./parser/real.dbus-org.freedesktop.nm-dispatcher.mjs", + ].map(file => import(file))); + const results = cases.forEach(({ string, ast }) => runTest(string, ast)); +})(); diff --git a/src/string-to-ast.mjs b/src/string-to-ast.mjs new file mode 100755 index 0000000..846534e --- /dev/null +++ b/src/string-to-ast.mjs @@ -0,0 +1,30 @@ +import Parser from "./Parser.mjs"; +import Lexer from "./Lexer.mjs"; +import visitorCreator from "./visitor-creator.mjs"; + +export default function stringToAst(input, c) { + const config = c || { strict: true }; + const parser = new Parser([], { outputCst: true }); + const lexingresult = Lexer.tokenize(input); + + if (config.strict && lexingresult.errors.length > 0) { + console.log(input); + console.dir(lexingresult, { depth: Infinity }); + throw new Error("Lexing errors detected") + } + + parser.input = lexingresult.tokens; + const cst = parser.unitFile(); + + if (config.strict && parser.errors.length > 0) { + console.log(input); + //console.dir(lexingresult, { depth: Infinity }); + console.dir(parser.errors, { depth: Infinity }); + throw new Error("Parsing errors detected") + } + + const Visitor = visitorCreator(parser); + const visitor = new Visitor(); + const ast = visitor.visit(cst); + return ast; +} diff --git a/src/string-to-data.mjs b/src/string-to-data.mjs new file mode 100755 index 0000000..66ef15b --- /dev/null +++ b/src/string-to-data.mjs @@ -0,0 +1,6 @@ +import stringToAst from "./string-to-ast.mjs"; +import astToData from "./astToData.mjs"; + +export default function stringToData(data) { + return astToData(stringToAst(data)); +} diff --git a/src/visitor.mjs b/src/visitor-creator.mjs old mode 100644 new mode 100755 similarity index 84% rename from src/visitor.mjs rename to src/visitor-creator.mjs index 0752c8f..f14ecc7 --- a/src/visitor.mjs +++ b/src/visitor-creator.mjs @@ -1,4 +1,4 @@ -export default (parser) => { +export default function visitorCreator(parser) { const BaseSQLVisitorWithDefaults = parser.getBaseCstVisitorConstructorWithDefaults(); return class UnitFileVisitor extends BaseSQLVisitorWithDefaults { @@ -10,7 +10,7 @@ export default (parser) => { _comment(ctx) { return { type: "comment", - value: ctx.Comment.map(({ image }) => image.replace(/^\s/, '').replace(/\s$/, '')).join(''), + value: ctx.Comment.map(({ image }) => image.replace(/^#\s?/, '').replace(/\s$/, '')).join(''), }; } @@ -18,10 +18,6 @@ export default (parser) => { return this._comment(ctx); } - commentLine(ctx) { - return this._comment(ctx); - } - propertyStatement(ctx) { const name = ctx.Property[0].image; return { @@ -33,7 +29,7 @@ export default (parser) => { } sectionStatement(ctx) { - const content = ctx.propertyStatement?.[0] || ctx.commentLine?.[0]; + const content = ctx.propertyStatement?.[0] || ctx.comment?.[0]; return this.visit(content); } @@ -58,4 +54,4 @@ export default (parser) => { }; } }; -}; +} diff --git a/test/examples/comment-only.mjs b/test/examples/comment-only.mjs deleted file mode 100644 index 7786ddd..0000000 --- a/test/examples/comment-only.mjs +++ /dev/null @@ -1,7 +0,0 @@ -export const content = `# Start of file comment`; - -export const result = { - type: 'unitFile', - comments: [ { type: 'comment', value: 'Start of file comment' } ], - sections: [] -}; diff --git a/test/spec.mjs b/test/spec.mjs deleted file mode 100644 index ecac941..0000000 --- a/test/spec.mjs +++ /dev/null @@ -1,67 +0,0 @@ -import glob from "glob"; -import { join } from "path"; -import { - Lexer, - Parser, - visitorCreator, -} from "../src/mod.mjs"; - -const runTest = (input, result) => { - const parser = new Parser([], { outputCst: true }); - const lexingresult = Lexer.tokenize(input); - - if (lexingresult.errors.length > 0) { - console.dir(lexingresult, { depth: Infinity }); - throw new Error("Lexing errors detected") - } - - parser.input = lexingresult.tokens; - const cst = parser.unitFile(); - - if (parser.errors.length > 0) { - console.dir(parser.errors, { depth: Infinity }); - throw new Error("Parsing errors detected") - } - - const Visitor = visitorCreator(parser); - const visitor = new Visitor(); - const ast = visitor.visit(cst); - - const resultString = JSON.stringify(ast, null, 2); - const expectedString = JSON.stringify(result, null, 2); - if (resultString !== expectedString) { - console.dir(ast, { depth: Infinity }); - console.dir(lexingresult, { depth: Infinity }); - throw new Error(`Mismatching result on testcase: -${input} - -=======Result====== - -${resultString} - -=======Expected====== - -${expectedString} - -`); - } -}; - -(async () => { - const files = await Promise.all( - process.argv - .slice(2) - .map(arg => new Promise( - (resolve, reject) => glob(arg, {}, (err, files) => { - if (err) { - reject(err); - } else { - resolve(files.map(file => join(process.cwd(), file))); - } - }) - )) - ); - - const cases = await Promise.all(files.flat().map(file => import(file))); - const results = cases.forEach(({ content, result }) => runTest(content, result)); -})();