#!/usr/bin/env python3 """Compiles a source file and checks errors against those listed in the file. Parameters: sys.argv[1]: a source file with contains the input and expected output sys.argv[2]: the Flang frontend driver sys.argv[3:]: Optional arguments to the Flang frontend driver""" import sys import re import tempfile import subprocess import common as cm from difflib import unified_diff # When messages are attached together, the source locations to which they # refer are not necessarily monotonically increasing. For example # error: foo.f90:10: There is a problem here # line 10 # because: foo.f90:12: This thing is invalid # line 12 (attached) # error: foo.f90:11: There is another problem here # line 11 # There is no way to represent that in the source file via ERROR annotations, # so before running unified_diff "canonicalize" the list of messages into an # order that corresponds to the line numbers. # # This also eliminates the issue with multiple messages emitted for the same # line: they can now be "expected" in the test file in any order, e.g. # !ERROR: Not enough arguments in a call to foo # !ERROR: `foo` is a subroutine, not a function # a = foo() # has the same effect as: # !ERROR: `foo` is a subroutine, not a function # !ERROR: Not enough arguments in a call to foo # a = foo() def join_per_line_map(m): """Take a map {"line_no:": [message1, message2, ...], ...} and convert it into a newline-separated string that follows the line ordering. """ # Sort messages for each line, and prepend the line number to each # message. Use numeric values of line numbers as keys to allow them # to be sorted numerically. sorted_lines_map = { int(k.rstrip(":")): [k + s for s in sorted(m[k])] for k in m.keys() } joined_lines_list = [] for line in sorted(sorted_lines_map.keys()): joined_lines_list.append("\n".join(sorted_lines_map[line])) return "\n".join(joined_lines_list) cm.check_args(sys.argv) srcdir = cm.set_source(sys.argv[1]) with open(srcdir, "r", encoding="utf-8") as f: src = f.readlines() diffs = "" log = "" flang_fc1 = cm.set_executable(sys.argv[2]) flang_fc1_args = sys.argv[3:] flang_fc1_options = "-fsyntax-only" # Compiles, and reads in the output from the compilation process cmd = [flang_fc1, *flang_fc1_args, flang_fc1_options, str(srcdir)] with tempfile.TemporaryDirectory() as tmpdir: try: proc = subprocess.run( cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, universal_newlines=True, cwd=tmpdir, encoding="utf-8", ) except subprocess.CalledProcessError as e: log = e.stderr if e.returncode >= 128: print(f"{log}") sys.exit(1) # Cleans up the output from the compilation process to be easier to process actual_per_line = dict() for line in log.split("\n"): m = re.search(r"[^:]*:(\d+:).*(?:error|warning|portability|because):(.*)", line) if m: if re.search(r"warning: .*fold.*host", line): continue # ignore host-dependent folding warnings line_colon = m.expand(r"\1") actual_per_line[line_colon] = actual_per_line.get(line_colon, []) + [ m.expand(r"\2") ] # Gets the expected errors and their line numbers expect_per_line = dict() errors = [] for i, line in enumerate(src, 1): m = re.search(r"(?:^\s*!\s*(?:ERROR|WARNING|PORTABILITY|BECAUSE): )(.*)", line) if m: errors.append(m.group(1)) continue if errors: expect_per_line[f"{i}:"] = [f" {x}" for x in errors] errors = [] actual = join_per_line_map(actual_per_line) expect = join_per_line_map(expect_per_line) # Compares the expected errors with the compiler errors for line in unified_diff(actual.split("\n"), expect.split("\n"), n=0): line = re.sub(r"(^\-)(\d+:)", r"\nactual at \g<2>", line) line = re.sub(r"(^\+)(\d+:)", r"\nexpect at \g<2>", line) diffs += line if diffs != "": print(diffs) print() print("FAIL") sys.exit(1) else: print() print("PASS")