Coverage for test.py: 100%

83 statements  

« prev     ^ index     » next       coverage.py v6.4, created at 2022-05-25 19:58 +0000

1#!/usr/bin/env python 

2 

3# Python Standard Library 

4import codeop 

5import doctest 

6import glob 

7import os 

8import shutil 

9import sys 

10import tempfile 

11 

12# Third-Party Libraries 

13import strictyaml 

14 

15# Test Files 

16# ------------------------------------------------------------------------------ 

17# Read mkdocs config file. 

18mkdocs_content = strictyaml.load(open("mkdocs.yml").read())["nav"].data 

19mkdocs_files = [] 

20for value in [list(item.values())[0] for item in mkdocs_content]: 

21 if isinstance(value, str): # page 

22 mkdocs_files.append(value) 

23 else: # section 

24 mkdocs_files.extend([list(item.values())[0] for item in value]) 

25mkdocs_files = ["mkdocs/" + file for file in mkdocs_files] 

26extra_testfiles = [] 

27test_files = mkdocs_files + extra_testfiles 

28 

29# Sandbox the Test Files 

30# ------------------------------------------------------------------------------ 

31# This is required: 

32# - to tweak the files before the tests, 

33# - to avoid the generation of artifacts (generated by the test code) 

34# in the current directory. 

35tmp_dir = tempfile.mkdtemp() # TODO: clean-up this directory 

36for filename in test_files: 

37 target_file = os.path.join(tmp_dir, filename) 

38 target_dir = os.path.dirname(target_file) 

39 os.makedirs(target_dir, exist_ok=True) 

40 shutil.copy(filename, target_file) 

41 

42# Tweak the Test Files 

43# ------------------------------------------------------------------------------ 

44# For each file, find the python fences, see if they are in interpreter mode 

45# or "code" mode. If they are in code mode, add the prompts then remove the  

46# fences and indent the code lines. 

47def promptize(src): 

48 "Add >>> or ... prompts to Python code" 

49 cc = codeop.compile_command # symbol="single" (the default here) 

50 # is required to deal with if / else constructs properly 

51 # (without going back to the ">>> " prompt after the if clause). 

52 lines = src.splitlines() 

53 output = [] 

54 chunk = [] 

55 for line in lines: 

56 if chunk == []: # new start 

57 output.append(">>> " + line) 

58 else: 

59 output.append("... " + line) 

60 chunk.append(line) 

61 try: 

62 code = cc("\n".join(chunk)) 

63 if code is not None: # full statement 

64 chunk = [] # start over 

65 except: # pragma: no cover 

66 raise 

67 assert len(lines) == len(output) 

68 return "\n".join(output) 

69 

70 

71def tweak(src): 

72 # Find code blocks with python fences, 

73 # add prompts when necessary, 

74 # then transform them into indented code blocks. 

75 lines = src.splitlines() 

76 chunks = {} 

77 start, end, code = None, None, [] 

78 for i, line in enumerate(lines): 

79 if line.startswith("```python"): 

80 start = i 

81 code.append("") 

82 elif line.startswith("```"): 

83 end = i + 1 

84 code.append("") 

85 assert end - start == len(code) 

86 chunks[(start, end)] = code 

87 code = [] 

88 elif code != []: 

89 code.append(line) 

90 

91 for loc, code in chunks.items(): 

92 chunk = "\n".join(code[1:-1]) # dont promptize initial and final newline 

93 if not chunk.strip().startswith(">>> "): # prompts are missing 

94 code[1:-1] = promptize(chunk).splitlines() 

95 code = [4 * " " + line for line in code] 

96 chunks[loc] = code 

97 

98 for (i, j), code in chunks.items(): 

99 lines[i:j] = code 

100 new_src = "\n".join(lines) 

101 return new_src 

102 

103 

104cwd = os.getcwd() 

105os.chdir(tmp_dir) 

106 

107for filename in test_files: 

108 with open(filename, encoding="utf-8") as file: 

109 src = file.read() 

110 src = tweak(src) 

111 with open(filename, "w", encoding="utf-8") as file: 

112 file.write(src) 

113 

114# Run the Tests 

115# ------------------------------------------------------------------------------ 

116verbose = "-v" in sys.argv or "--verbose" in sys.argv 

117build = "-b" in sys.argv or "--build" in sys.argv # build documentation images 

118 

119# Setup and teardown the src code (theming & figure cleanup) 

120if build: # pragma: no cover 

121 for filename in test_files: 

122 with open(filename, encoding="utf-8") as file: 

123 src = file.read() 

124 src = """ 

125 >>> import seaborn; seaborn.set_theme(style="whitegrid", font="Roboto") 

126 >>> import matplotlib.pyplot 

127 

128""" + src + """ 

129 >>> matplotlib.pyplot.close('all') 

130 

131""" 

132 with open(filename, "w", encoding="utf-8") as file: 

133 file.write(src) 

134 

135fails = 0 

136tests = 0 

137for filename in test_files: 

138 options = {"module_relative": False, "verbose": verbose} 

139 _fails, _tests = doctest.testfile(filename, **options) 

140 fails += _fails 

141 tests += _tests 

142 

143# Copy the generated images to the images folder 

144if build: # pragma: no cover 

145 for image in glob.glob("*svg"): 

146 dest_fpath = cwd + "/mkdocs/images/" + image 

147 os.makedirs(os.path.dirname(dest_fpath), exist_ok=True) 

148 shutil.copy(image, dest_fpath) 

149 

150os.chdir(cwd) 

151 

152if fails > 0 or verbose: # pragma: no cover 

153 print() 

154 print(60 * "-") 

155 print("Test Suite Report:", end=" ") 

156 print("{0} failures / {1} tests".format(fails, tests)) 

157 print(60 * "-") 

158if fails: # pragma: no cover 

159 sys.exit(1)