Skip to content
This repository was archived by the owner on Apr 14, 2022. It is now read-only.

Commit 47b8c7d

Browse files
authored
Add test generation script (#436)
* add test generation script * remove unused imports * use SemaphoreSlim and async func instead of lock and Task.Run
1 parent ed51fa3 commit 47b8c7d

File tree

1 file changed

+362
-0
lines changed

1 file changed

+362
-0
lines changed

src/gen.py

Lines changed: 362 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,362 @@
1+
#!/usr/bin/env python
2+
3+
import argparse
4+
import contextlib
5+
from collections import defaultdict
6+
import string
7+
import sys
8+
import os.path
9+
10+
11+
def main():
12+
script_path = os.path.realpath(__file__)
13+
script_dir = os.path.dirname(script_path)
14+
default_input = os.path.join(
15+
script_dir, "UnitTests", "TestData", "gen", "completion"
16+
)
17+
default_output = os.path.join(
18+
script_dir, "Analysis", "Engine", "Test", "GenTests.cs"
19+
)
20+
21+
parser = argparse.ArgumentParser(
22+
description="Generate completion and hover tests",
23+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
24+
)
25+
parser.add_argument(
26+
"--ignore",
27+
type=str,
28+
help="comma separated list of tests to disable, of the form <filename>(:<linenum>)",
29+
)
30+
parser.add_argument(
31+
"--only", type=str, help="comma separated list of tests to generate"
32+
)
33+
parser.add_argument(
34+
"-o",
35+
"--out",
36+
nargs="?",
37+
type=argparse.FileType("w"),
38+
default=default_output,
39+
help="output file",
40+
)
41+
parser.add_argument(
42+
"-i",
43+
"--input",
44+
type=str,
45+
default=default_input,
46+
help="location of completions directory",
47+
)
48+
args = parser.parse_args()
49+
50+
if args.only:
51+
to_generate = set(args.only.split(","))
52+
else:
53+
to_generate = set(DEFAULT_TEST_FILES)
54+
55+
line_skip = defaultdict(set)
56+
57+
if args.ignore:
58+
for i in args.ignore.split(","):
59+
if ":" not in i:
60+
to_generate.discard(i)
61+
else:
62+
name, line = i.split(":")
63+
64+
try:
65+
line = int(line)
66+
except:
67+
print(f"error in format of ignored item {i}", file=sys.stderr)
68+
return
69+
70+
line_skip[name].add(line)
71+
72+
to_generate = sorted(to_generate)
73+
74+
with contextlib.redirect_stdout(args.out):
75+
print(PREAMBLE)
76+
77+
for name in to_generate:
78+
filename = os.path.join(args.input, name + ".py")
79+
ignored_lines = line_skip[name]
80+
create_tests(name, filename, ignored_lines)
81+
82+
print(POSTAMBLE)
83+
84+
85+
def create_tests(name, filename, ignored_lines):
86+
camel_name = snake_to_camel(name)
87+
88+
with open(filename) as fp:
89+
lines = fp.read().splitlines()
90+
91+
width = len(str(len(lines)))
92+
93+
tests = []
94+
95+
for i, line in enumerate(lines):
96+
if i in ignored_lines:
97+
continue
98+
99+
line: str = line.strip()
100+
if not line.startswith("#?"):
101+
continue
102+
103+
line = line[2:].strip()
104+
105+
next_line = lines[i + 1]
106+
col = len(next_line)
107+
108+
if " " in line:
109+
maybe_num = line.split(" ", 1)
110+
111+
try:
112+
col = int(maybe_num[0])
113+
line = maybe_num[1]
114+
except ValueError:
115+
pass
116+
117+
filt = next_line[:col].lstrip()
118+
filt = select_filter(filt, ". {[(")
119+
120+
args = line.strip()
121+
func_name = "Line_{0:0{pad}}".format(i + 1, pad=width)
122+
func_name = camel_name + "_" + func_name
123+
124+
tmpl = COMPLETION_TEST if args.startswith("[") else HOVER_TEST
125+
tests.append(
126+
tmpl.format(
127+
name=func_name,
128+
module=csharp_str(name),
129+
line=i + 1,
130+
col=col,
131+
args=csharp_str(args),
132+
filter=csharp_str(filt),
133+
)
134+
)
135+
136+
if tests:
137+
print(CLASS_PREAMBLE.format(name=camel_name))
138+
for t in tests:
139+
print(t)
140+
print(CLASS_POSTAMBLE)
141+
142+
143+
DEFAULT_TEST_FILES = [
144+
"arrays",
145+
"async_",
146+
"basic",
147+
"classes",
148+
"completion",
149+
"complex",
150+
"comprehensions",
151+
"context",
152+
"decorators",
153+
"definition",
154+
"descriptors",
155+
"docstring",
156+
"dynamic_arrays",
157+
"dynamic_params",
158+
"flow_analysis",
159+
"fstring",
160+
"functions",
161+
"generators",
162+
"imports",
163+
"invalid",
164+
"isinstance",
165+
"keywords",
166+
"lambdas",
167+
"named_param",
168+
"on_import",
169+
"ordering",
170+
"parser",
171+
"pep0484_basic",
172+
"pep0484_comments",
173+
"pep0484_typing",
174+
"pep0526_variables",
175+
"precedence",
176+
"recursion",
177+
"stdlib",
178+
"sys_path",
179+
"types",
180+
]
181+
182+
PREAMBLE = """// Python Tools for Visual Studio
183+
// Copyright(c) Microsoft Corporation
184+
// All rights reserved.
185+
//
186+
// Licensed under the Apache License, Version 2.0 (the License); you may not use
187+
// this file except in compliance with the License. You may obtain a copy of the
188+
// License at http://www.apache.org/licenses/LICENSE-2.0
189+
//
190+
// THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS
191+
// OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY
192+
// IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
193+
// MERCHANTABILITY OR NON-INFRINGEMENT.
194+
//
195+
// See the Apache Version 2.0 License for specific language governing
196+
// permissions and limitations under the License.
197+
198+
using System;
199+
using System.Collections.Concurrent;
200+
using System.Collections.Generic;
201+
using System.IO;
202+
using System.Linq;
203+
using System.Threading;
204+
using System.Threading.Tasks;
205+
using AnalysisTests;
206+
using FluentAssertions;
207+
using Microsoft.Python.LanguageServer.Implementation;
208+
using Microsoft.PythonTools.Analysis;
209+
using Microsoft.PythonTools.Analysis.FluentAssertions;
210+
using Microsoft.PythonTools.Interpreter;
211+
using Microsoft.PythonTools.Parsing;
212+
using Microsoft.VisualStudio.TestTools.UnitTesting;
213+
using TestUtilities;
214+
215+
namespace GenTests {"""
216+
217+
POSTAMBLE = """
218+
public class GenTest : ServerBasedTest {
219+
private static Server _server;
220+
private static readonly SemaphoreSlim _sem = new SemaphoreSlim(1, 1);
221+
private static readonly InterpreterConfiguration _interpreter = PythonVersions.LatestAvailable3X;
222+
private static readonly PythonLanguageVersion _version = _interpreter.Version.ToLanguageVersion();
223+
private static readonly ConcurrentDictionary<string, Task> _opened = new ConcurrentDictionary<string, Task>();
224+
225+
private async Task<Server> SharedServer() {
226+
if (_server != null) {
227+
return _server;
228+
}
229+
230+
await _sem.WaitAsync();
231+
try {
232+
var root = new Uri(TestData.GetPath("TestData", "gen", "completion"));
233+
_server = await CreateServerAsync(_interpreter, root);
234+
} finally {
235+
_sem.Release();
236+
}
237+
238+
return _server;
239+
}
240+
241+
protected async Task<Uri> OpenAndWait(string module) {
242+
var server = await SharedServer();
243+
244+
var src = TestData.GetPath("TestData", "gen", "completion", module + ".py");
245+
var uri = new Uri(src);
246+
247+
await _opened.GetOrAdd(src, f => server.SendDidOpenTextDocument(uri, File.ReadAllText(f)));
248+
await server.WaitForCompleteAnalysisAsync(CancellationToken.None);
249+
250+
return uri;
251+
}
252+
253+
protected async Task DoCompletionTest(string module, int lineNum, int col, string args, string filter) {
254+
var server = await SharedServer();
255+
256+
var tests = string.IsNullOrWhiteSpace(args) ? new List<string>() : ParseStringList(args);
257+
var uri = await OpenAndWait(module);
258+
259+
var res = await server.SendCompletion(uri, lineNum, col);
260+
var items = res.items?.Select(item => item.insertText).Where(t => t.Contains(filter)).ToList() ?? new List<string>();
261+
262+
if (tests.Count == 0) {
263+
items.Should().BeEmpty();
264+
} else {
265+
items.Should().Contain(tests);
266+
}
267+
}
268+
269+
protected async Task DoHoverTest(string module, int lineNum, int col, string args) {
270+
var server = await SharedServer();
271+
272+
var tests = string.IsNullOrWhiteSpace(args)
273+
? new List<string>()
274+
: args.Split(' ', options: StringSplitOptions.RemoveEmptyEntries).Select(s => s.EndsWith("()") ? s.Substring(0, s.Length - 2) : s).ToList();
275+
276+
var uri = await OpenAndWait(module);
277+
278+
var res = await server.SendHover(uri, lineNum, col);
279+
280+
if (tests.Count == 0) {
281+
res.contents.value.Should().BeEmpty();
282+
} else {
283+
res.contents.value.Should().ContainAll(tests);
284+
}
285+
}
286+
287+
protected List<string> ParseStringList(string s) {
288+
var list = new List<string>();
289+
290+
using (var reader = new StringReader(s)) {
291+
var tokenizer = new Tokenizer(_version);
292+
tokenizer.Initialize(reader);
293+
294+
while (!tokenizer.IsEndOfFile) {
295+
var token = tokenizer.GetNextToken();
296+
297+
if (token.Kind == TokenKind.EndOfFile) {
298+
break;
299+
}
300+
301+
switch (token.Kind) {
302+
case TokenKind.Constant when token != Tokens.NoneToken && (token.Value is string || token.Value is AsciiString):
303+
list.Add(token.Image);
304+
break;
305+
}
306+
}
307+
}
308+
309+
return list;
310+
}
311+
}
312+
}"""
313+
314+
CLASS_PREAMBLE = """ [TestClass]
315+
public class {name}Tests : GenTest {{
316+
public TestContext TestContext {{ get; set; }}
317+
318+
[TestInitialize]
319+
public void TestInitialize() => TestEnvironmentImpl.TestInitialize($"{{TestContext.FullyQualifiedTestClassName}}.{{TestContext.TestName}}");
320+
321+
[TestCleanup]
322+
public void TestCleanup() => TestEnvironmentImpl.TestCleanup();"""
323+
324+
CLASS_POSTAMBLE = """
325+
}"""
326+
327+
COMPLETION_TEST = """
328+
[TestMethod, Priority(0)] public async Task {name}_Completion() => await DoCompletionTest({module}, {line}, {col}, {args}, {filter});"""
329+
330+
331+
HOVER_TEST = """
332+
[TestMethod, Priority(0)] public async Task {name}_Hover() => await DoHoverTest({module}, {line}, {col}, {args});"""
333+
334+
335+
def snake_to_camel(s):
336+
return string.capwords(s, "_").replace("_", "")
337+
338+
339+
def select_filter(s, cs):
340+
found = False
341+
for c in cs:
342+
i = s.rfind(c)
343+
if i != -1:
344+
found = True
345+
s = s[i + 1 :]
346+
347+
if found:
348+
return s
349+
return ""
350+
351+
352+
def csharp_str(s):
353+
if s is None:
354+
return "null"
355+
356+
s = s.replace('"', '""')
357+
return '@"{}"'.format(s)
358+
359+
360+
if __name__ == "__main__":
361+
main()
362+

0 commit comments

Comments
 (0)