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

Commit 9a4b6a6

Browse files
authored
Merge pull request #552 from kazk/python-no-concat
Add Python 3 unittest runner without concatenation
2 parents dbf943d + 205eade commit 9a4b6a6

File tree

3 files changed

+289
-4
lines changed

3 files changed

+289
-4
lines changed

frameworks/python/codewars.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import unittest
2+
import traceback
3+
from time import perf_counter
4+
5+
class CodewarsTestRunner(object):
6+
def __init__(self): pass
7+
def run(self, test):
8+
r = CodewarsTestResult()
9+
s = perf_counter()
10+
print("\n<DESCRIBE::>Tests")
11+
try:
12+
test(r)
13+
finally:
14+
pass
15+
print("\n<COMPLETEDIN::>{:.4f}".format(1000*(perf_counter() - s)))
16+
return r
17+
18+
__unittest = True
19+
class CodewarsTestResult(unittest.TestResult):
20+
def __init__(self):
21+
super().__init__()
22+
self.start = 0.0
23+
24+
def startTest(self, test):
25+
print("\n<IT::>" + test._testMethodName)
26+
super().startTest(test)
27+
self.start = perf_counter()
28+
29+
def stopTest(self, test):
30+
print("\n<COMPLETEDIN::>{:.4f}".format(1000*(perf_counter() - self.start)))
31+
super().stopTest(test)
32+
33+
def addSuccess(self, test):
34+
print("\n<PASSED::>Test Passed")
35+
super().addSuccess(test)
36+
37+
def addError(self, test, err):
38+
print("\n<ERROR::>Unhandled Exception")
39+
print("\n<LOG:ESC:Error>" + esc(''.join(traceback.format_exception_only(err[0], err[1]))))
40+
print("\n<LOG:ESC:Traceback>" + esc(self._exc_info_to_string(err, test)))
41+
super().addError(test, err)
42+
43+
def addFailure(self, test, err):
44+
print("\n<FAILED::>Test Failed")
45+
print("\n<LOG:ESC:Failure>" + esc(''.join(traceback.format_exception_only(err[0], err[1]))))
46+
super().addFailure(test, err)
47+
48+
# from unittest/result.py
49+
def _exc_info_to_string(self, err, test):
50+
exctype, value, tb = err
51+
# Skip test runner traceback levels
52+
while tb and self._is_relevant_tb_level(tb):
53+
tb = tb.tb_next
54+
if exctype is test.failureException:
55+
length = self._count_relevant_tb_levels(tb) # Skip assert*() traceback levels
56+
else:
57+
length = None
58+
return ''.join(traceback.format_tb(tb, limit=length))
59+
60+
def _is_relevant_tb_level(self, tb):
61+
return '__unittest' in tb.tb_frame.f_globals
62+
63+
def _count_relevant_tb_levels(self, tb):
64+
length = 0
65+
while tb and not self._is_relevant_tb_level(tb):
66+
length += 1
67+
tb = tb.tb_next
68+
return length
69+
70+
def esc(s):
71+
return s.replace("\n", "<:LF:>")

lib/runners/python.js

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
"use strict";
22

3+
const writeFileSync = require('../utils/write-file-sync');
4+
35
module.exports = {
46
solutionOnly(opts, runCode) {
57
runVersion(opts, [opts.setup, opts.solution].join("\n"), runCode);
68
},
79
testIntegration(opts, runCode) {
10+
if (isNoConcat(opts)) return python3unittest(opts, runCode);
811
var code;
912
switch (opts.testFramework) {
1013
case 'cw':
1114
case 'cw-2':
1215
code = opts.projectMode ? cw2Project(opts) : cw2Code(opts);
1316
break;
1417
case 'unittest':
15-
// TODO: support projectMode for unittest, which should require
16-
// improving unittest support so that a specific test case called Test doesn't need
17-
// to be used
18+
// TODO: support projectMode for unittest, which should require
19+
// improving unittest support so that a specific test case called Test doesn't need
20+
// to be used
1821
code = unittestCode(opts);
1922
break;
2023
default:
@@ -23,6 +26,7 @@ module.exports = {
2326
runVersion(opts, code, runCode);
2427
},
2528
sanitizeStdErr(opts, err) {
29+
if (isNoConcat(opts)) return err;
2630
// get rid of some of the noisy content. We remove line numbers since
2731
// they don't match up to what the user sees and will only confuse them.
2832
return err
@@ -123,3 +127,59 @@ print("<DESCRIBE::>Tests")
123127
unittest.TestLoader().loadTestsFromTestCase(Test).run(reload(__import__('unittestwrapper')).CwTestResult())
124128
print("<COMPLETEDIN::>")
125129
`;
130+
131+
// Returns true if running tests without concatenation is possible.
132+
function isNoConcat(opts) {
133+
return opts.testFramework === 'unittest' && isPython3(opts);
134+
}
135+
136+
// .
137+
// |-- setup.py
138+
// |-- solution.py
139+
// `-- test
140+
// |-- __init__.py
141+
// |-- __main__.py
142+
// `-- test_solution.py
143+
// inspired by http://stackoverflow.com/a/27630375
144+
//
145+
// for backward compatibility:
146+
// - prepend `import unittest` to fixture if missing
147+
// - prepend `from solution import *` to fixture to simulate concatenation
148+
// - prepend `from setup import *` to solution to simulate concatenation
149+
function python3unittest(opts, runCode) {
150+
let solution = opts.solution;
151+
if (opts.setup) {
152+
writeFileSync(opts.dir, 'setup.py', opts.setup);
153+
if (!/^\s*import setup\s*$/m.test(solution) && !/^\s*from setup\s+/m.test(solution)) {
154+
solution = 'from setup import *\n' + solution;
155+
}
156+
}
157+
writeFileSync(opts.dir, 'solution.py', solution);
158+
let fixture = opts.fixture;
159+
if (!/^\s*import\s+unittest/m.test(fixture)) fixture = 'import unittest\n' + fixture;
160+
if (!/^\s*import solution\s*$/m.test(fixture) && !/^\s*from solution\s+/m.test(fixture)) {
161+
fixture = 'from solution import *\n' + fixture;
162+
}
163+
writeFileSync(opts.dir+'/test', 'test_solution.py', fixture);
164+
writeFileSync(opts.dir+'/test', '__init__.py', '');
165+
writeFileSync(opts.dir+'/test', '__main__.py', [
166+
'import unittest',
167+
'from codewars import CodewarsTestRunner',
168+
'',
169+
'def load_tests(loader, tests, pattern):',
170+
' return loader.discover(".")',
171+
'',
172+
'unittest.main(testRunner=CodewarsTestRunner())',
173+
'',
174+
].join('\n'));
175+
runCode({
176+
name: pythonCmd(opts),
177+
args: ['test'],
178+
options: {
179+
cwd: opts.dir,
180+
env: Object.assign({}, process.env, {
181+
PYTHONPATH: `/runner/frameworks/python:${process.env.PYTHONPATH}`
182+
}),
183+
}
184+
});
185+
}

test/runners/python_spec.js

Lines changed: 155 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
11
var expect = require('chai').expect;
22
var runner = require('../runner');
3+
const exec = require('child_process').exec;
34

45
describe('python runner', function() {
6+
afterEach(function cleanup(done) {
7+
exec([
8+
'rm -rf',
9+
'/home/codewarrior/*.py',
10+
'/home/codewarrior/__pycache__',
11+
'/home/codewarrior/test',
12+
].join(' '), function(err) {
13+
if (err) return done(err);
14+
return done();
15+
});
16+
});
17+
518
// These specs are compatible with both Python 2 and 3
619
['2', '3', '3.6'].forEach(lv => {
720
describe('.run', function() {
@@ -178,7 +191,12 @@ describe('python runner', function() {
178191
].join('\n'),
179192
testFramework: 'unittest'
180193
}, function(buffer) {
181-
expect(buffer.stdout).to.contain('\n<ERROR::>Unhandled Exception: exceptions are my favorite, I always throw them\n');
194+
if (lv.startsWith('3')) {
195+
expect(buffer.stdout).to.contain('\n<ERROR::>Unhandled Exception');
196+
}
197+
else {
198+
expect(buffer.stdout).to.contain('\n<ERROR::>Unhandled Exception: exceptions are my favorite, I always throw them\n');
199+
}
182200
done();
183201
});
184202
});
@@ -187,6 +205,18 @@ describe('python runner', function() {
187205
});
188206

189207
describe('Output format commands', function() {
208+
afterEach(function cleanup(done) {
209+
exec([
210+
'rm -rf',
211+
'/home/codewarrior/*.py',
212+
'/home/codewarrior/__pycache__',
213+
'/home/codewarrior/test',
214+
].join(' '), function(err) {
215+
if (err) return done(err);
216+
return done();
217+
});
218+
});
219+
190220
for (const v of ['2', '3', '3.6']) {
191221
it(`should be on independent lines (Python${v} cw-2)`, function(done) {
192222
runner.run({
@@ -225,3 +255,127 @@ describe('Output format commands', function() {
225255
});
226256
}
227257
});
258+
259+
describe('unittest no concat', function() {
260+
afterEach(function cleanup(done) {
261+
exec([
262+
'rm -rf',
263+
'/home/codewarrior/*.py',
264+
'/home/codewarrior/__pycache__',
265+
'/home/codewarrior/test',
266+
].join(' '), function(err) {
267+
if (err) return done(err);
268+
return done();
269+
});
270+
});
271+
272+
for (const v of ['3.x', '3.6']) {
273+
it(`should handle a basic assertion (${v})`, function(done) {
274+
runner.run({
275+
language: 'python',
276+
languageVersion: v,
277+
testFramework: 'unittest',
278+
solution: [
279+
'def add(a, b): return a + b'
280+
].join('\n'),
281+
fixture: [
282+
// Test class name is no longer restricted.
283+
'class TestAddition(unittest.TestCase):',
284+
' def test_add(self):',
285+
' self.assertEqual(add(1, 1), 2)'
286+
].join('\n'),
287+
}, function(buffer) {
288+
expect(buffer.stdout).to.contain('\n<PASSED::>');
289+
done();
290+
});
291+
});
292+
293+
it(`should handle a failed assetion (${v})`, function(done) {
294+
runner.run({
295+
language: 'python',
296+
languageVersion: v,
297+
testFramework: 'unittest',
298+
solution: [
299+
'def add(a, b): return a - b'
300+
].join('\n'),
301+
fixture: [
302+
'class TestAddition(unittest.TestCase):',
303+
' def test_add(self):',
304+
' self.assertEqual(add(1, 1), 2)',
305+
''
306+
].join('\n'),
307+
}, function(buffer) {
308+
expect(buffer.stdout).to.contain('\n<FAILED::>');
309+
done();
310+
});
311+
});
312+
313+
it(`syntax error in solution show line numbers (${v})`, function(done) {
314+
runner.run({
315+
language: 'python',
316+
languageVersion: v,
317+
testFramework: 'unittest',
318+
solution: [
319+
'def add(a, b): return a b'
320+
].join('\n'),
321+
fixture: [
322+
'class TestAddition(unittest.TestCase):',
323+
' def test_add(self):',
324+
' self.assertEqual(add(1, 1), 2)',
325+
''
326+
].join('\n'),
327+
}, function(buffer) {
328+
expect(buffer.stdout).to.contain('\n<ERROR::>');
329+
expect(buffer.stdout).to.contain('solution.py", line 1');
330+
expect(buffer.stdout).to.contain('SyntaxError');
331+
done();
332+
});
333+
});
334+
335+
it(`should handle error (${v})`, function(done) {
336+
runner.run({
337+
language: 'python',
338+
languageVersion: v,
339+
testFramework: 'unittest',
340+
solution: [
341+
'def add(a, b): return a / b'
342+
].join('\n'),
343+
fixture: [
344+
'class TestAddition(unittest.TestCase):',
345+
' def test_add(self):',
346+
' self.assertEqual(add(1, 0), 1)',
347+
''
348+
].join('\n'),
349+
}, function(buffer) {
350+
expect(buffer.stdout).to.contain('\n<ERROR::>');
351+
done();
352+
});
353+
});
354+
355+
it(`should handle tests in multiple suites (${v})`, function(done) {
356+
// combined into one suite
357+
runner.run({
358+
language: 'python',
359+
languageVersion: v,
360+
testFramework: 'unittest',
361+
solution: [
362+
'def add(a, b): return a + b'
363+
].join('\n'),
364+
fixture: [
365+
'class TestAddition1(unittest.TestCase):',
366+
' def test_add1(self):',
367+
' self.assertEqual(add(1, 1), 2)',
368+
'',
369+
'class TestAddition2(unittest.TestCase):',
370+
' def test_add2(self):',
371+
' self.assertEqual(add(2, 2), 4)',
372+
'',
373+
].join('\n'),
374+
}, function(buffer) {
375+
expect(buffer.stdout).to.contain('\n<IT::>test_add1');
376+
expect(buffer.stdout).to.contain('\n<IT::>test_add2');
377+
done();
378+
});
379+
});
380+
}
381+
});

0 commit comments

Comments
 (0)