From 7c3259bc7f13ba4b2a95d340e3b485f222b4ebd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20B=C3=A4dorf?= Date: Wed, 25 Nov 2020 03:11:59 +0100 Subject: [PATCH] Initial commit --- .gitignore | 2 + package-lock.json | 109 ++++++++++++++++++ package.json | 18 +++ src/Lexer.mjs | 78 +++++++++++++ src/Parser.mjs | 66 +++++++++++ src/mod.mjs | 3 + src/visitor.mjs | 53 +++++++++ test/examples/basic.mjs | 21 ++++ test/examples/blueman-tray.service | 13 +++ test/examples/comment-only.mjs | 7 ++ test/examples/comments.mjs | 50 ++++++++ test/examples/gammastep.service | 13 +++ test/examples/keepassxc.service | 12 ++ test/examples/libinput-gestures.service | 13 +++ test/examples/mako.service | 13 +++ test/examples/multiline-value.mjs | 48 ++++++++ test/examples/nextcloud.service | 12 ++ test/examples/nm-applet.service | 13 +++ .../polkit-gnome-authentication-agent.service | 13 +++ test/examples/repeated-settings.mjs | 36 ++++++ test/examples/scream-ivshmem-pulse.service | 13 +++ test/examples/sway.service | 14 +++ test/examples/swayidle.service | 18 +++ test/examples/waybar.service | 13 +++ test/examples/weird-characters.mjs | 22 ++++ test/examples/xdg-desktop-portal-wlr.service | 10 ++ test/examples/xdg-desktop-portal.service | 7 ++ test/examples/xsettingsd.service | 14 +++ test/examples/ydotool.service | 16 +++ test/spec.mjs | 79 +++++++++++++ 30 files changed, 799 insertions(+) create mode 100644 .gitignore create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/Lexer.mjs create mode 100644 src/Parser.mjs create mode 100644 src/mod.mjs create mode 100644 src/visitor.mjs create mode 100644 test/examples/basic.mjs create mode 100644 test/examples/blueman-tray.service create mode 100644 test/examples/comment-only.mjs create mode 100644 test/examples/comments.mjs create mode 100644 test/examples/gammastep.service create mode 100644 test/examples/keepassxc.service create mode 100644 test/examples/libinput-gestures.service create mode 100644 test/examples/mako.service create mode 100644 test/examples/multiline-value.mjs create mode 100644 test/examples/nextcloud.service create mode 100644 test/examples/nm-applet.service create mode 100644 test/examples/polkit-gnome-authentication-agent.service create mode 100644 test/examples/repeated-settings.mjs create mode 100644 test/examples/scream-ivshmem-pulse.service create mode 100644 test/examples/sway.service create mode 100644 test/examples/swayidle.service create mode 100644 test/examples/waybar.service create mode 100644 test/examples/weird-characters.mjs create mode 100644 test/examples/xdg-desktop-portal-wlr.service create mode 100644 test/examples/xdg-desktop-portal.service create mode 100644 test/examples/xsettingsd.service create mode 100644 test/examples/ydotool.service create mode 100644 test/spec.mjs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..93cab34 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +yarn-error.log diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1a53c35 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,109 @@ +{ + "name": "unitfile-parser", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chevrotain": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-7.0.3.tgz", + "integrity": "sha512-G634q7M5EiqNNv+0MKcQES2jmqabbi4PvUDpzjG2t+i1XQFaMCz0o8BZ8lbQbZex4RqkzJ3pOy+UwNLFlQm4eg==", + "requires": { + "regexp-to-ast": "0.5.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "regexp-to-ast": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", + "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..474aa69 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "unitfile-parser", + "version": "1.0.0", + "description": "Parse systemd unit files", + "main": "src/mod.mjs", + "type": "module", + "scripts": { + "test": "node ./test/spec.mjs './test/examples/*.mjs'" + }, + "author": "", + "license": "ISC", + "dependencies": { + "chevrotain": "^7.0.3" + }, + "devDependencies": { + "glob": "^7.1.6" + } +} diff --git a/src/Lexer.mjs b/src/Lexer.mjs new file mode 100644 index 0000000..37d6441 --- /dev/null +++ b/src/Lexer.mjs @@ -0,0 +1,78 @@ +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; + } + } + } + + return [text.substr(startOffset)]; + } +} + +export const SectionHeading = createToken({ + name: "SectionHeading", + pattern: /\[[A-z]+\]/, +}); +export const Property = createToken({ + name: "Property", + pattern: /[A-z][A-z]+=/, + push_mode: "value_mode", +}); +export const Value = createToken({ + name: "Value", + pattern: { exec: getValueParser('\n', '#') }, + line_breaks: true, + pop_mode: true, +}); +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, +}); +export const WhiteSpace = createToken({ + name: "WhiteSpace", + pattern: /\s+/, + group: Lexer.SKIPPED, +}); + +export const tokens = [ + CommentStart, + Value, + Comment, + SectionHeading, + Property, + WhiteSpace, +]; + +export default new Lexer({ + defaultMode: "line_mode", + modes: { + line_mode: [ + CommentStart, + SectionHeading, + Property, + WhiteSpace, + ], + value_mode: [ Value ], + comment_mode: [ Comment ], + }, +}); diff --git a/src/Parser.mjs b/src/Parser.mjs new file mode 100644 index 0000000..ec56777 --- /dev/null +++ b/src/Parser.mjs @@ -0,0 +1,66 @@ +import chevrotain from "chevrotain"; +const { CstParser } = chevrotain; +import { + tokens, + Comment, + CommentStart, + Property, + SectionHeading, + Value, +} from "./Lexer.mjs"; + +export default class UnitFileParser extends CstParser { + constructor() { + super(tokens); + const $ = this; + + $.RULE("comment", () => { + $.CONSUME(CommentStart); + $.CONSUME(Comment); + }); + + $.RULE("value", () => { + $.CONSUME(Value); + }); + + $.RULE("sectionHeadingStatement", () => { + $.CONSUME(SectionHeading); + $.OPTION(() => { + $.SUBRULE($.comment); + }); + }); + + $.RULE("propertyStatement", () => { + $.CONSUME(Property); + $.SUBRULE($.value); + $.OPTION(() => { + $.SUBRULE($.comment); + }); + }); + + $.RULE("sectionStatement", () => { + $.OR([ + { ALT: () => $.SUBRULE($.propertyStatement) }, + { ALT: () => $.SUBRULE($.comment) }, + ]); + }); + + $.RULE("section", () => { + $.SUBRULE($.sectionHeadingStatement); + $.MANY(() => { + $.SUBRULE($.sectionStatement); + }); + }); + + $.RULE("unitFile", () => { + $.MANY(() => { + $.OR([ + { ALT: () => $.SUBRULE($.section) }, + { ALT: () => $.SUBRULE($.comment) }, + ]); + }); + }); + + this.performSelfAnalysis(); + } +} diff --git a/src/mod.mjs b/src/mod.mjs new file mode 100644 index 0000000..c84ec57 --- /dev/null +++ b/src/mod.mjs @@ -0,0 +1,3 @@ +export { default as Lexer } from "./Lexer.mjs"; +export { default as Parser } from "./Parser.mjs"; +export { default as visitorCreator } from "./visitor.mjs"; diff --git a/src/visitor.mjs b/src/visitor.mjs new file mode 100644 index 0000000..eabe696 --- /dev/null +++ b/src/visitor.mjs @@ -0,0 +1,53 @@ +export default (parser) => { + const BaseSQLVisitorWithDefaults = parser.getBaseCstVisitorConstructorWithDefaults(); + + return class UnitFileVisitor extends BaseSQLVisitorWithDefaults { + constructor() { + super(); + this.validateVisitor(); + }; + + comment(ctx) { + return { + type: "comment", + value: ctx.Comment.map(({ image }) => image.replace(/^\s/, '').replace(/\s$/, '')).join(''), + }; + } + + propertyStatement(ctx) { + const name = ctx.Property[0].image; + return { + type: "setting", + name: name.substring(0, name.length - 1), + value: ctx.value.map(v => v.children.Value.map(({ image }) => image.replace(/\s$/, '')).join('')).join(''), + comment: this.visit(ctx.comment?.[0]), + }; + } + + sectionStatement(ctx) { + const content = ctx.propertyStatement?.[0] || ctx.comment?.[0]; + return this.visit(content); + } + + section(ctx) { + const titleString = ctx.sectionHeadingStatement[0].children.SectionHeading[0].image; + const titleComment = this.visit(ctx.sectionHeadingStatement[0].children.comment?.[0]) + return { + type: "section", + title: titleString.substring(1, titleString.length - 1), + titleComment, + body: ctx.sectionStatement?.map(this.visit.bind(this)) || [], + }; + } + + unitFile(ctx) { + const comments = ctx.comment?.map(comment => this.visit(comment)) || []; + const sections = ctx.section?.map(section => this.visit(section)) || []; + return { + type: "unitFile", + comments, + sections, + }; + } + }; +}; diff --git a/test/examples/basic.mjs b/test/examples/basic.mjs new file mode 100644 index 0000000..5c59662 --- /dev/null +++ b/test/examples/basic.mjs @@ -0,0 +1,21 @@ +export const content = `[Unit] +Description=Idle manager for Wayland`; + +export const result = { + type: 'unitFile', + comments: [], + sections: [ + { + type: 'section', + title: 'Unit', + titleComment: undefined, + body: [ + { + type: 'setting', + name: 'Description', + value: 'Idle manager for Wayland' + } + ] + } + ] +}; diff --git a/test/examples/blueman-tray.service b/test/examples/blueman-tray.service new file mode 100644 index 0000000..bb1d52a --- /dev/null +++ b/test/examples/blueman-tray.service @@ -0,0 +1,13 @@ +[Unit] +Description=a status icon application for blueman +Documentation=man:blueman-tray(1) +BindsTo=graphical-session.target +Wants=graphical-session-pre.target +After=graphical-session-pre.target + +[Service] +Type=simple +ExecStart=/usr/bin/blueman-tray + +[Install] +WantedBy=sway-session.target diff --git a/test/examples/comment-only.mjs b/test/examples/comment-only.mjs new file mode 100644 index 0000000..7786ddd --- /dev/null +++ b/test/examples/comment-only.mjs @@ -0,0 +1,7 @@ +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/examples/comments.mjs b/test/examples/comments.mjs new file mode 100644 index 0000000..dcf09c1 --- /dev/null +++ b/test/examples/comments.mjs @@ -0,0 +1,50 @@ +export const content = `# Outside comment +[Unit] # Heading comment +# Inline comment +Description=Idle manager for Wayland # End of value comment +ExecStart=echo\ + "some string" # End of value comment +Alias=asdf#Comment Without spaces + +[Install] +# Comment only in this body +`; + +export const result = { + type: 'unitFile', + comments: [ { type: 'comment', value: 'Outside comment' } ], + sections: [ + { + type: 'section', + title: 'Unit', + titleComment: { type: 'comment', value: 'Heading comment' }, + body: [ + { type: 'comment', value: 'Inline comment' }, + { + type: 'setting', + name: 'Description', + value: 'Idle manager for Wayland', + comment: { type: 'comment', value: 'End of value comment' } + }, + { + type: 'setting', + name: 'ExecStart', + value: 'echo "some string"', + comment: { type: 'comment', value: 'End of value comment' } + }, + { + type: 'setting', + name: 'Alias', + value: 'asdf', + comment: { type: 'comment', value: 'Comment Without spaces' } + } + ] + }, + { + type: 'section', + title: 'Install', + titleComment: { type: 'comment', value: 'Comment only in this body' }, + body: [] + } + ] +}; diff --git a/test/examples/gammastep.service b/test/examples/gammastep.service new file mode 100644 index 0000000..c7f03d9 --- /dev/null +++ b/test/examples/gammastep.service @@ -0,0 +1,13 @@ +[Unit] +Description=set color temperature of display according to time of day +Documentation=man:gammastep(1) +BindsTo=graphical-session.target +Wants=graphical-session-pre.target +After=graphical-session-pre.target + +[Service] +Type=simple +ExecStart=/sbin/gammastep -l geoclue2 -m wayland -v + +[Install] +WantedBy=sway-session.target diff --git a/test/examples/keepassxc.service b/test/examples/keepassxc.service new file mode 100644 index 0000000..12aa0fa --- /dev/null +++ b/test/examples/keepassxc.service @@ -0,0 +1,12 @@ +[Unit] +Description=Keepassxc + +[Service] +Type=simple +ExecStart=keepassxc +ExecReload=/bin/kill -HUP $MAINPID +KillMode=process +Restart=on-failure + +[Install] +WantedBy=default.target diff --git a/test/examples/libinput-gestures.service b/test/examples/libinput-gestures.service new file mode 100644 index 0000000..3cafbfc --- /dev/null +++ b/test/examples/libinput-gestures.service @@ -0,0 +1,13 @@ +[Unit] +Description=Actions gestures on your touchpad using libinput +Documentation=https://github.com/bulletmark/libinput-gestures +BindsTo=graphical-session.target +Wants=graphical-session-pre.target +After=graphical-session-pre.target + +[Service] +Type=simple +ExecStart=python3 /usr/bin/libinput-gestures + +[Install] +WantedBy=sway-session.target diff --git a/test/examples/mako.service b/test/examples/mako.service new file mode 100644 index 0000000..871818b --- /dev/null +++ b/test/examples/mako.service @@ -0,0 +1,13 @@ +[Unit] +Description=A lightweight Wayland notification daemon +Documentation=man:mako(1) +BindsTo=graphical-session.target +Wants=graphical-session-pre.target +After=graphical-session-pre.target + +[Service] +Type=simple +ExecStart=/usr/bin/mako + +[Install] +WantedBy=sway-session.target diff --git a/test/examples/multiline-value.mjs b/test/examples/multiline-value.mjs new file mode 100644 index 0000000..51331a3 --- /dev/null +++ b/test/examples/multiline-value.mjs @@ -0,0 +1,48 @@ +export const content = ` +[Service] +ExecStart=/usr/bin/swayidle -w \\ + timeout 600 'swaylock-bg' \\ + timeout 900 'swaymsg "output * dpms off"' \\ + resume 'swaymsg "output * dpms on"' \\ + after-resume 'swaylock-bg' +ExecStop=/usr/bin/swayidle -w \\ + after-resume 'swaylock-bg' # Comment at the end +ExecPause=/usr/bin/swayidle -w \\ + after-resume 'swaylock-bg'#No space comment +`; + +export const result = { + type: 'unitFile', + comments: [], + sections: [ + { + type: 'section', + title: 'Service', + titleComment: undefined, + body: [ + { + type: 'setting', + name: 'ExecStart', + value: '/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'", + comment: undefined + }, + { + type: 'setting', + name: 'ExecStop', + value: "/usr/bin/swayidle -w \\\n after-resume 'swaylock-bg'", + comment: { type: 'comment', value: 'Comment at the end' } + }, + { + type: 'setting', + name: 'ExecPause', + value: "/usr/bin/swayidle -w \\\n after-resume 'swaylock-bg'", + comment: { type: 'comment', value: 'No space comment' } + } + ] + } + ] +}; diff --git a/test/examples/nextcloud.service b/test/examples/nextcloud.service new file mode 100644 index 0000000..d078ef6 --- /dev/null +++ b/test/examples/nextcloud.service @@ -0,0 +1,12 @@ +[Unit] +Description=Nextcloud Client + +[Service] +Type=simple +ExecStart=/usr/bin/nextcloud --background +ExecReload=/bin/kill -HUP $MAINPID +KillMode=process +Restart=on-failure + +[Install] +WantedBy=default.target diff --git a/test/examples/nm-applet.service b/test/examples/nm-applet.service new file mode 100644 index 0000000..4b99841 --- /dev/null +++ b/test/examples/nm-applet.service @@ -0,0 +1,13 @@ +[Unit] +Description=network monitor and control GUI applet +Documentation=man:nm-applet(1) +BindsTo=graphical-session.target +Wants=graphical-session-pre.target +After=graphical-session-pre.target + +[Service] +Type=simple +ExecStart=/usr/bin/nm-applet --indicator + +[Install] +WantedBy=sway-session.target diff --git a/test/examples/polkit-gnome-authentication-agent.service b/test/examples/polkit-gnome-authentication-agent.service new file mode 100644 index 0000000..43fbaa1 --- /dev/null +++ b/test/examples/polkit-gnome-authentication-agent.service @@ -0,0 +1,13 @@ +[Unit] +Description=Legacy polkit authentication agent for GNOME +Documentation=https://gitlab.freedesktop.org/polkit/polkit/ +BindsTo=graphical-session.target +Wants=graphical-session-pre.target +After=graphical-session-pre.target + +[Service] +Type=simple +ExecStart=/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1 + +[Install] +WantedBy=sway-session.target diff --git a/test/examples/repeated-settings.mjs b/test/examples/repeated-settings.mjs new file mode 100644 index 0000000..70575a7 --- /dev/null +++ b/test/examples/repeated-settings.mjs @@ -0,0 +1,36 @@ +export const content = `[Install] +After=something.service +After=something-else.service +After=something-different.service`; + +export const result = { + type: 'unitFile', + comments: [], + sections: [ + { + type: 'section', + title: 'Install', + titleComment: undefined, + body: [ + { + type: 'setting', + name: 'After', + value: 'something.service', + comment: undefined + }, + { + type: 'setting', + name: 'After', + value: 'something-else.service', + comment: undefined + }, + { + type: 'setting', + name: 'After', + value: 'something-different.service', + comment: undefined + } + ] + } + ] +}; diff --git a/test/examples/scream-ivshmem-pulse.service b/test/examples/scream-ivshmem-pulse.service new file mode 100644 index 0000000..1ff00f7 --- /dev/null +++ b/test/examples/scream-ivshmem-pulse.service @@ -0,0 +1,13 @@ +[Unit] +Description=Scream IVSHMEM pulse reciever +After=pulseaudio.service +Wants=pulseaudio.service + +[Service] +Type=simple +ExecStartPre=/usr/bin/truncate -s 0 /dev/shm/scream-ivshmem +ExecStartPre=/usr/bin/dd if=/dev/zero of=/dev/shm/scream-ivshmem bs=1M count=2 +ExecStart=/usr/bin/scream -m /dev/shm/scream-ivshmem + +[Install] +WantedBy=default.target diff --git a/test/examples/sway.service b/test/examples/sway.service new file mode 100644 index 0000000..0adf2eb --- /dev/null +++ b/test/examples/sway.service @@ -0,0 +1,14 @@ +[Unit] +Description=sway - SirCmpwn's Wayland window manager +Documentation=man:sway(5) +BindsTo=graphical-session.target +Wants=graphical-session-pre.target +After=graphical-session-pre.target + +[Service] +Type=simple +EnvironmentFile=-%h/.config/sway/env +ExecStart=/usr/bin/sway -d +Restart=on-failure +RestartSec=1 +TimeoutStopSec=10 diff --git a/test/examples/swayidle.service b/test/examples/swayidle.service new file mode 100644 index 0000000..a556952 --- /dev/null +++ b/test/examples/swayidle.service @@ -0,0 +1,18 @@ +# Start of file comment +[Unit] # Title comment +Description=Idle manager for Wayland +Documentation=man:swayidle(1) +BindsTo=graphical-session.target +Wants=graphical-session-pre.target +After=graphical-session-pre.target + +[Service] +Type=simple +ExecStart=/usr/bin/swayidle -w \ + timeout 600 'swaylock-bg' \ + timeout 900 'swaymsg "output * dpms off"' \ + resume 'swaymsg "output * dpms on"' \ + after-resume 'swaylock-bg' + +[Install] +WantedBy=sway-session.target diff --git a/test/examples/waybar.service b/test/examples/waybar.service new file mode 100644 index 0000000..77d3be3 --- /dev/null +++ b/test/examples/waybar.service @@ -0,0 +1,13 @@ +[Unit] +Description=Highly customizable Wayland bar for Sway and Wlroots based compositors. +Documentation=https://github.com/Alexays/Waybar/wiki/ +BindsTo=graphical-session.target +Wants=graphical-session-pre.target +After=graphical-session-pre.target + +[Service] +Type=simple +ExecStart=/usr/bin/waybar + +[Install] +WantedBy=sway-session.target diff --git a/test/examples/weird-characters.mjs b/test/examples/weird-characters.mjs new file mode 100644 index 0000000..ede9a4e --- /dev/null +++ b/test/examples/weird-characters.mjs @@ -0,0 +1,22 @@ +export const content = `[Install] +Description=Test description(1) https://something/? \\# # test`; + +export const result = { + type: 'unitFile', + comments: [], + sections: [ + { + type: 'section', + title: 'Install', + titleComment: undefined, + body: [ + { + type: 'setting', + name: 'Description', + value: 'Test description(1) https://something/? \\#', + comment: { type: 'comment', value: 'test' } + } + ] + } + ] +}; diff --git a/test/examples/xdg-desktop-portal-wlr.service b/test/examples/xdg-desktop-portal-wlr.service new file mode 100644 index 0000000..74edbd2 --- /dev/null +++ b/test/examples/xdg-desktop-portal-wlr.service @@ -0,0 +1,10 @@ +[Unit] +Description=Portal service (wlroots implementation) +PartOf=graphical-session.target +After=graphical-session.target + +[Service] +Type=dbus +BusName=org.freedesktop.impl.portal.desktop.wlr +ExecStart=/usr/lib/xdg-desktop-portal-wlr +Restart=on-failure diff --git a/test/examples/xdg-desktop-portal.service b/test/examples/xdg-desktop-portal.service new file mode 100644 index 0000000..29368f2 --- /dev/null +++ b/test/examples/xdg-desktop-portal.service @@ -0,0 +1,7 @@ +[Unit] +Description=Portal service + +[Service] +Type=dbus +BusName=org.freedesktop.portal.Desktop +ExecStart=/usr/lib/xdg-desktop-portal diff --git a/test/examples/xsettingsd.service b/test/examples/xsettingsd.service new file mode 100644 index 0000000..938b619 --- /dev/null +++ b/test/examples/xsettingsd.service @@ -0,0 +1,14 @@ +[Unit] +Description=X Settings Daemon +Documentation=https://github.com/derat/xsettingsd/wiki/Installation +BindsTo=graphical-session.target +Wants=graphical-session-pre.target +After=graphical-session-pre.target + +[Service] +Type=simple +ExecStart=/usr/bin/xsettingsd +ExecStop=/usr/bin/env pkill xsettingsd + +[Install] +WantedBy=sway-session.target diff --git a/test/examples/ydotool.service b/test/examples/ydotool.service new file mode 100644 index 0000000..a6310f6 --- /dev/null +++ b/test/examples/ydotool.service @@ -0,0 +1,16 @@ +[Unit] +Description=ydotool - Generic command-line automation tool (no X!) +Documentation=https://github.com/ReimuNotMoe/ydotool +BindsTo=graphical-session.target +Wants=graphical-session-pre.target +After=graphical-session-pre.target + +[Service] +Type=simple +ExecStart=/usr/bin/ydotoold +Restart=on-failure +RestartSec=1 +TimeoutStopSec=10 + +[Install] +WantedBy=sway-session.target diff --git a/test/spec.mjs b/test/spec.mjs new file mode 100644 index 0000000..2029b9b --- /dev/null +++ b/test/spec.mjs @@ -0,0 +1,79 @@ +import glob from "glob"; +import { join } from "path"; +import { + Lexer, + Parser, + visitorCreator, +} from "../src/mod.mjs"; + +const runTest = (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; +}; + +(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.map( + ({ content, result }) => { + const ast = runTest(content); + const resultString = JSON.stringify(ast); + const expectedString = JSON.stringify(result); + if (resultString !== expectedString) { + console.dir(ast, { depth: Infinity }); + throw new Error(`Mismatching result on testcase: +${content} + +=======Result====== + +${resultString} + +=======Expected====== + +${expectedString} + +`); + } + + return null; + }, + ) + .filter(r => r !== null); + + if (results.length) { + results.forEach(err => console.error(err)); + throw new Error(`${results.length} failed tests`); + } +})();