Skip to content

Commit 4adcd24

Browse files
authored
Merge pull request #15 from xeger/tests
Create a basic integration test
2 parents e958548 + 8f7a327 commit 4adcd24

File tree

11 files changed

+1012
-31
lines changed

11 files changed

+1012
-31
lines changed

.github/workflows/main.yml

+102-20
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,116 @@
11
name: Main
22
on:
3-
- push
4-
- pull_request_target
3+
- push
4+
- pull_request_target
55
jobs:
66
ci:
77
name: CI
8-
runs-on: ubuntu-latest
8+
strategy:
9+
matrix:
10+
os:
11+
# - macos-latest
12+
- ubuntu-latest
13+
# - windows-latest
14+
ruby:
15+
- 3.1
16+
# - 3.0
17+
# - 2.7
18+
runs-on: ${{ matrix.os }}
919
env:
1020
CI: true
1121
steps:
12-
- uses: actions/checkout@master
13-
- uses: actions/setup-node@v3
14-
with:
15-
node-version: 14.x
16-
cache: yarn
17-
- name: Compile
18-
run: |
19-
yarn install --frozen-lockfile
20-
yarn compile
22+
- uses: actions/checkout@master
23+
- uses: actions/setup-node@v3
24+
with:
25+
node-version: 14.x
26+
cache: yarn
27+
- uses: ruby/setup-ruby@v1
28+
with:
29+
bundler-cache: true
30+
ruby-version: ${{ matrix.ruby }}
31+
- name: Install gem
32+
run: gem install syntax_tree
33+
- name: Compile extension
34+
run: |
35+
yarn install --frozen-lockfile
36+
yarn compile
37+
- name: Setup GUI Environment
38+
run: |
39+
sudo apt-get install -yq dbus-x11 ffmpeg > /dev/null
40+
mkdir -p ~/bin
41+
mkdir -p ~/var/run
42+
cat <<EOF > ~/bin/xvfb-shim
43+
#! /bin/bash
44+
echo DISPLAY=\$DISPLAY >> ${GITHUB_ENV}
45+
echo XAUTHORITY=\$XAUTHORITY >> ${GITHUB_ENV}
46+
sleep 86400
47+
EOF
48+
chmod a+x ~/bin/xvfb-shim
49+
dbus-launch >> ${GITHUB_ENV}
50+
start-stop-daemon --start --quiet --pidfile ~/var/run/Xvfb.pid --make-pidfile --background --exec /usr/bin/xvfb-run -- ~/bin/xvfb-shim
51+
echo -n "Waiting for Xvfb to start..."
52+
while ! grep -q DISPLAY= ${GITHUB_ENV}; do
53+
echo -n .
54+
sleep 3
55+
done
56+
if: runner.os == 'Linux'
57+
- name: Start Screen Recording
58+
run: |
59+
mkdir -p $PWD/videos-raw
60+
no_close=--no-close # uncomment to see ffmpeg output (i.e. leave stdio open)
61+
start-stop-daemon $no_close --start --quiet --pidfile ~/var/run/ffmpeg.pid --make-pidfile --background --exec /usr/bin/ffmpeg -- -nostdin -f x11grab -video_size 1280x1024 -framerate 10 -i ${DISPLAY}.0+0,0 $PWD/videos-raw/test.mp4
62+
# pid=`cat ~/var/run/ffmpeg.pid`
63+
# echo "Waiting for ffmpeg (pid $pid) to start recording (display $DISPLAY)..."
64+
# while [ ! -f $PWD/videos-raw/test.mp4 ]; do
65+
# echo -n .
66+
# sleep 3
67+
# done
68+
if: runner.os == 'Linux'
69+
- name: Cache VS Code Binary
70+
id: vscode-test
71+
uses: actions/cache@v3
72+
with:
73+
path: .vscode-test/
74+
key: ${{ runner.os }}-vscode-test
75+
# - name: SSH Debug Breakpoint
76+
# uses: lhotari/action-upterm@v1
77+
# with:
78+
# limit-access-to-actor: true
79+
- name: Run Tests
80+
run: npm test
81+
- name: Stop Screen Recording
82+
run: |
83+
start-stop-daemon --stop --pidfile ~/var/run/ffmpeg.pid
84+
sleep 3
85+
mkdir -p $PWD/videos
86+
for f in $PWD/videos-raw/*.mp4; do
87+
out=`basename $f`
88+
ffmpeg -i $f -vf format=yuv420p $PWD/videos/$out
89+
done
90+
if: always() && runner.os == 'Linux'
91+
- name: Archive Screen Recording
92+
uses: actions/upload-artifact@v3
93+
with:
94+
name: videos
95+
path: |
96+
videos/
97+
if: always() && runner.os == 'Linux'
98+
- name: Teardown GUI Environment
99+
run: |
100+
start-stop-daemon --stop --pidfile ~/var/run/Xvfb.pid
101+
kill $DBUS_SESSION_BUS_PID
102+
if: always() && runner.os == 'Linux'
21103
automerge:
22104
name: AutoMerge
23105
needs: ci
24106
runs-on: ubuntu-latest
25107
if: github.event_name == 'pull_request_target' && (github.actor == github.repository_owner || github.actor == 'dependabot[bot]')
26108
steps:
27-
- uses: actions/github-script@v6
28-
with:
29-
script: |
30-
github.pulls.merge({
31-
owner: context.payload.repository.owner.login,
32-
repo: context.payload.repository.name,
33-
pull_number: context.payload.pull_request.number
34-
})
109+
- uses: actions/github-script@v6
110+
with:
111+
script: |
112+
github.pulls.merge({
113+
owner: context.payload.repository.owner.login,
114+
repo: context.payload.repository.name,
115+
pull_number: context.payload.pull_request.number
116+
})

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.token
2+
.vscode-test
23
node_modules
34
*.vsix
45
out

.vscode/launch.json

+13
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
{
66
"version": "0.2.0",
77
"configurations": [
8+
89
{
910
"name": "Run Extension",
1011
"type": "extensionHost",
@@ -17,5 +18,17 @@
1718
],
1819
"preLaunchTask": "${defaultBuildTask}"
1920
},
21+
{
22+
"name": "Run Extension Tests",
23+
"type": "extensionHost",
24+
"request": "launch",
25+
"runtimeExecutable": "${execPath}",
26+
"args": [
27+
"--extensionDevelopmentPath=${workspaceFolder}",
28+
"--extensionTestsPath=${workspaceFolder}/out/test/suite/index"
29+
],
30+
"outFiles": ["${workspaceFolder}/out/test/**/*.js"],
31+
"preLaunchTask": "${defaultBuildTask}"
32+
}
2033
]
2134
}

package.json

+6
Original file line numberDiff line numberDiff line change
@@ -79,15 +79,21 @@
7979
"compile": "tsc -p ./",
8080
"package": "vsce package --yarn --githubBranch main",
8181
"publish": "vsce publish --yarn --githubBranch main",
82+
"test": "node ./out/test/runTest.js",
8283
"vscode:prepublish": "yarn compile",
8384
"watch": "tsc --watch -p ./"
8485
},
8586
"dependencies": {
8687
"vscode-languageclient": "8.0.2"
8788
},
8889
"devDependencies": {
90+
"@types/glob": "^7.1.1",
91+
"@types/mocha": "^9.1.1",
8992
"@types/node": "^18.0.0",
9093
"@types/vscode": "^1.68.0",
94+
"@vscode/test-electron": "^1.6.1",
95+
"glob": "^7.1.4",
96+
"mocha": "^9.1.1",
9197
"typescript": "^4.7.4",
9298
"vsce": "^2.9.2"
9399
}

src/extension.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ import Visualize from "./Visualize";
1010
const promiseExec = promisify(exec);
1111

1212
// This object will get initialized once the language client is ready. It will
13-
// get set back to null when the extension is deactivated.
14-
let languageClient: LanguageClient | null = null;
13+
// get set back to null when the extension is deactivated. It is exported for
14+
// easier testing.
15+
export let languageClient: LanguageClient | null = null;
1516

1617
// This is the expected top-level export that is called by VSCode when the
1718
// extension is activated.
@@ -54,6 +55,10 @@ export async function activate(context: ExtensionContext) {
5455
// This function is called when the extension is activated or when the
5556
// language server is restarted.
5657
async function startLanguageServer() {
58+
if (languageClient) {
59+
return; // preserve idempotency
60+
}
61+
5762
// The top-level configuration group is syntaxTree. All of the configuration
5863
// for the extension is under that group.
5964
const config = workspace.getConfiguration("syntaxTree");
@@ -142,8 +147,6 @@ export async function activate(context: ExtensionContext) {
142147
startLanguageServer();
143148
break;
144149
}
145-
if (action === 'Restart') {
146-
}
147150
}
148151
}
149152

@@ -154,6 +157,7 @@ export async function activate(context: ExtensionContext) {
154157
if (languageClient) {
155158
outputChannel.appendLine("Stopping language server...");
156159
await languageClient.stop();
160+
languageClient = null;
157161
}
158162
}
159163

src/test/runTest.ts

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { runTests } from '@vscode/test-electron';
2+
import * as path from 'path';
3+
4+
import { USER_DATA_DIR, WORKSPACE_DIR } from './suite/setup';
5+
6+
async function main() {
7+
try {
8+
// The folder containing the Extension Manifest package.json
9+
// Passed to `--extensionDevelopmentPath`
10+
const extensionDevelopmentPath = path.resolve(__dirname, '../../');
11+
12+
// The path to the extension test script
13+
// Passed to --extensionTestsPath
14+
const extensionTestsPath = path.resolve(__dirname, './suite/index');
15+
16+
// Download VS Code, unzip it and run the integration test
17+
await runTests({ extensionDevelopmentPath, extensionTestsPath, launchArgs: ['--disable-extensions', '--disable-gpu', '--user-data-dir', USER_DATA_DIR, WORKSPACE_DIR] });
18+
} catch (err) {
19+
console.error('Failed to run tests');
20+
process.exit(1);
21+
}
22+
}
23+
24+
main();

src/test/suite/automation.ts

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import * as assert from 'assert';
2+
import * as path from 'path';
3+
import { TextEncoder } from 'util';
4+
5+
import { Uri, commands, window, workspace } from 'vscode';
6+
7+
import { WORKSPACE_DIR } from './setup';
8+
9+
export async function reset() {
10+
await commands.executeCommand('workbench.action.closeAllEditors');
11+
}
12+
13+
export async function createEditor(content: string) {
14+
const filename = `${Math.random().toString().slice(2)}.rb`;
15+
const uri = Uri.file(`${WORKSPACE_DIR}${path.sep}${filename}`);
16+
await workspace.fs.writeFile(uri, new TextEncoder().encode(content));
17+
await window.showTextDocument(uri);
18+
assert.ok(window.activeTextEditor);
19+
assert.equal(window.activeTextEditor.document.getText(), content);
20+
return window.activeTextEditor;
21+
}
22+
23+
export function findNewestEditor() {
24+
return window.visibleTextEditors[window.visibleTextEditors.length - 1];
25+
}
26+
27+
export function formatDocument() {
28+
return commands.executeCommand('editor.action.formatDocument', 'ruby-syntax-tree.vscode-syntax-tree');
29+
}
30+
31+
export function restart() {
32+
return commands.executeCommand('syntaxTree.restart');
33+
}
34+
35+
export function start() {
36+
return commands.executeCommand('syntaxTree.start');
37+
}
38+
39+
export function stop() {
40+
return commands.executeCommand('syntaxTree.stop');
41+
}
42+
43+
export function visualize() {
44+
return commands.executeCommand('syntaxTree.visualize');
45+
}

src/test/suite/index.test.ts

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import * as assert from 'assert';
2+
import { before, beforeEach } from 'mocha';
3+
import { State } from 'vscode-languageclient';
4+
5+
import * as auto from './automation';
6+
import * as extension from '../../extension';
7+
8+
const UNFORMATTED = `class Foo; def bar; puts 'baz'; end; end`;
9+
10+
const FORMATTED = `class Foo
11+
def bar
12+
puts "baz"
13+
end
14+
end
15+
`;
16+
17+
suite('Syntax Tree', () => {
18+
beforeEach(auto.reset);
19+
20+
suite('lifecycle commands', () => {
21+
test('start', async () => {
22+
await auto.start();
23+
assert.notEqual(extension.languageClient, null)
24+
assert.equal(extension.languageClient?.state, State.Running)
25+
});
26+
27+
test('stop', async () => {
28+
await auto.start();
29+
await auto.stop();
30+
assert.equal(extension.languageClient, null)
31+
})
32+
33+
test('restart', async () => {
34+
await auto.restart();
35+
assert.notEqual(extension.languageClient, null)
36+
assert.equal(extension.languageClient?.state, State.Running)
37+
})
38+
});
39+
40+
suite('functional commands', () => {
41+
before(auto.start);
42+
43+
test('format', async () => {
44+
const editor = await auto.createEditor(UNFORMATTED);
45+
await auto.formatDocument();
46+
assert.equal(editor.document.getText(), FORMATTED);
47+
});
48+
49+
test('visualize', async () => {
50+
await auto.createEditor(UNFORMATTED);
51+
await auto.visualize();
52+
const editor = auto.findNewestEditor();
53+
assert.match(editor.document.getText(), /^\(program/);
54+
})
55+
})
56+
});

0 commit comments

Comments
 (0)