Skip to content

Commit f1bfb02

Browse files
committed
[xctest_checker] Inline expectations
Enhance xctest_checker such that it can parse "CHECK" prefixes that are in the middle of a line (instead of just at the very beginning). In addition, use a common `XCTestCheckerError` to ensure all functional test suite failures are displayed inline in Xcode.
1 parent 9ff1724 commit f1bfb02

File tree

4 files changed

+96
-25
lines changed

4 files changed

+96
-25
lines changed

Tests/Functional/SingleFailingTestCase/main.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ class SingleFailingTestCase: XCTestCase {
1919
]
2020
}
2121

22-
// CHECK: Test Case 'SingleFailingTestCase.test_fails' started at \d+:\d+:\d+\.\d+
23-
// CHECK: .*/SingleFailingTestCase/main.swift:26: error: SingleFailingTestCase.test_fails : XCTAssertTrue failed -
24-
// CHECK: Test Case 'SingleFailingTestCase.test_fails' failed \(\d+\.\d+ seconds\).
22+
// CHECK: Test Case 'SingleFailingTestCase.test_fails' started at \d+:\d+:\d+\.\d+
23+
// CHECK: .*/SingleFailingTestCase/main.swift:26: error: SingleFailingTestCase.test_fails : XCTAssertTrue failed -
24+
// CHECK: Test Case 'SingleFailingTestCase.test_fails' failed \(\d+\.\d+ seconds\).
2525
func test_fails() {
2626
XCTAssert(false)
2727
}

Tests/Functional/xctest_checker/tests/test_compare.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import unittest
1313

1414
from xctest_checker import compare
15+
from xctest_checker.error import XCTestCheckerError
1516

1617

1718
def _tmpfile(content):
@@ -26,30 +27,52 @@ class CompareTestCase(unittest.TestCase):
2627
def test_no_match_raises(self):
2728
actual = _tmpfile('foo\nbar\nbaz\n')
2829
expected = _tmpfile('c: foo\nc: baz\nc: bar\n')
29-
with self.assertRaises(AssertionError):
30+
with self.assertRaises(XCTestCheckerError):
3031
compare.compare(actual, expected, check_prefix='c: ')
3132

32-
def test_too_few_expected_raises(self):
33+
def test_too_few_expected_raises_and_first_line_in_error(self):
3334
actual = _tmpfile('foo\nbar\nbaz\n')
3435
expected = _tmpfile('c: foo\nc: bar\n')
35-
with self.assertRaises(AssertionError):
36+
with self.assertRaises(XCTestCheckerError) as cm:
3637
compare.compare(actual, expected, check_prefix='c: ')
3738

38-
def test_too_many_expected_raises(self):
39+
self.assertIn('{}:{}'.format(expected, 1), cm.exception.message)
40+
41+
def test_too_many_expected_raises_and_excess_check_line_in_error(self):
3942
actual = _tmpfile('foo\nbar\n')
4043
expected = _tmpfile('c: foo\nc: bar\nc: baz\n')
41-
with self.assertRaises(AssertionError):
44+
with self.assertRaises(XCTestCheckerError) as cm:
4245
compare.compare(actual, expected, check_prefix='c: ')
4346

47+
self.assertIn('{}:{}'.format(expected, 3), cm.exception.message)
48+
4449
def test_match_does_not_raise(self):
4550
actual = _tmpfile('foo\nbar\nbaz\n')
4651
expected = _tmpfile('c: foo\nc: bar\nc: baz\n')
4752
compare.compare(actual, expected, check_prefix='c: ')
4853

54+
def test_match_with_inline_check_does_not_raise(self):
55+
actual = _tmpfile('bling\nblong\n')
56+
expected = _tmpfile('meep meep // c: bling\nmeep\n// c: blong\n')
57+
compare.compare(actual, expected, check_prefix='// c: ')
58+
59+
def test_check_prefix_twice_in_the_same_line_raises_with_line(self):
60+
actual = _tmpfile('blorp\nbleep\n')
61+
expected = _tmpfile('c: blorp\nc: bleep c: blammo\n')
62+
with self.assertRaises(XCTestCheckerError) as cm:
63+
compare.compare(actual, expected, check_prefix='c: ')
64+
65+
self.assertIn('{}:{}'.format(expected, 2), cm.exception.message)
66+
67+
def test_check_prefix_in_run_line_ignored(self):
68+
actual = _tmpfile('flim\n')
69+
expected = _tmpfile('// RUN: xctest_checker --prefix "c: "\nc: flim\n')
70+
compare.compare(actual, expected, check_prefix='c: ')
71+
4972
def test_includes_file_name_and_line_of_expected_in_error(self):
5073
actual = _tmpfile('foo\nbar\nbaz\n')
5174
expected = _tmpfile('c: foo\nc: baz\nc: bar\n')
52-
with self.assertRaises(AssertionError) as cm:
75+
with self.assertRaises(XCTestCheckerError) as cm:
5376
compare.compare(actual, expected, check_prefix='c: ')
5477

5578
self.assertIn("{}:{}:".format(expected, 2), cm.exception.message)

Tests/Functional/xctest_checker/xctest_checker/compare.py

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
import re
1212

13+
from .error import XCTestCheckerError
14+
1315

1416
def _actual_lines(path):
1517
"""
@@ -26,37 +28,64 @@ def _expected_lines_and_line_numbers(path, check_prefix):
2628
that begins with the given prefix.
2729
"""
2830
with open(path) as f:
29-
for line_number, line in enumerate(f):
30-
if line.startswith(check_prefix):
31-
yield line[len(check_prefix):].strip(), line_number+1
31+
for index, line in enumerate(f):
32+
if 'RUN:' in line:
33+
# Ignore lit directives, which may include a call to
34+
# xctest_checker that specifies a check prefix.
35+
continue
36+
37+
# Note that line numbers are not zero-indexed; we must add one to
38+
# the loop index.
39+
line_number = index + 1
40+
41+
components = line.split(check_prefix)
42+
if len(components) == 2:
43+
yield components[1].strip(), line_number
44+
elif len(components) > 2:
45+
# Include a newline, then the file name and line number in the
46+
# exception in order to have it appear as an inline failure in
47+
# Xcode.
48+
raise XCTestCheckerError(
49+
path, line_number,
50+
'Usage violation: prefix "{}" appears twice in the same '
51+
'line.'.format(check_prefix))
52+
3253

3354
def _add_whitespace_leniency(original_regex):
3455
return "^ *" + original_regex + " *$"
3556

57+
3658
def compare(actual, expected, check_prefix):
3759
"""
3860
Compares each line in the two given files.
3961
If any line in the 'actual' file doesn't match the regex in the 'expected'
4062
file, raises an AssertionError. Also raises an AssertionError if the number
4163
of lines in the two files differ.
4264
"""
43-
for actual_line, expected_line_and_line_number in map(
65+
for actual_line, expected_line_and_number in map(
4466
None,
4567
_actual_lines(actual),
4668
_expected_lines_and_line_numbers(expected, check_prefix)):
4769

48-
if actual_line is None:
49-
raise AssertionError('There were more lines expected to appear '
50-
'than there were lines in the actual input.')
51-
if expected_line_and_line_number is None:
52-
raise AssertionError('There were more lines than expected to '
53-
'appear.')
70+
if expected_line_and_number is None:
71+
raise XCTestCheckerError(
72+
expected, 1,
73+
'The actual output contained more lines of text than the '
74+
'expected output. First unexpected line: {}'.format(
75+
repr(actual_line)))
5476

55-
(expected_line, expectation_source_line_number) = expected_line_and_line_number
77+
(expected_line, expectation_line_number) = expected_line_and_number
78+
79+
if actual_line is None:
80+
raise XCTestCheckerError(
81+
expected, expectation_line_number,
82+
'There were more lines expected to appear than there were '
83+
'lines in the actual input. Unmet expectation: {}'.format(
84+
repr(expected_line)))
5685

5786
if not re.match(_add_whitespace_leniency(expected_line), actual_line):
58-
raise AssertionError('Actual line did not match the expected '
59-
'regular expression.\n'
60-
'{}:{}: Actual: {}\n'
61-
'Expected: {}\n'.format(
62-
expected, expectation_source_line_number, repr(actual_line), repr(expected_line)))
87+
raise XCTestCheckerError(
88+
expected, expectation_line_number,
89+
'Actual line did not match the expected regular expression.\n'
90+
'Actual: {}\nExpected: {}'.format(
91+
repr(actual_line), repr(expected_line)))
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# xctest_checker/error.py - Errors that display nicely in Xcode -*- python -*-
2+
#
3+
# This source file is part of the Swift.org open source project
4+
#
5+
# Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors
6+
# Licensed under Apache License v2.0 with Runtime Library Exception
7+
#
8+
# See http://swift.org/LICENSE.txt for license information
9+
# See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
11+
12+
class XCTestCheckerError(Exception):
13+
"""
14+
An exception that indicates an xctest_checker-based functional test should
15+
fail. Formats exception messages such that they render inline in Xcode.
16+
"""
17+
def __init__(self, path, line_number, message):
18+
super(XCTestCheckerError, self).__init__(
19+
'\n{}:{}: {}'.format(path, line_number, message))

0 commit comments

Comments
 (0)