-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathprob_checker.py
More file actions
355 lines (268 loc) · 12.1 KB
/
prob_checker.py
File metadata and controls
355 lines (268 loc) · 12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
#!/usr/bin/env python3
# (C) Copyright 2021-2022 Hewlett Packard Enterprise Development LP
"""
CodeWars - Check Problem Script
Notes:
* REQUIRES python3 to run
* Expects data sets to be in a local subdirectory called `student_datasets`
* Works with .py files
"""
__version__ = "0.8.2022.02.28.1.modded"
__author__ = "Sebastian Schagerer"
__modifier__ = "Isaac Deter"
"""
Now with modifications to make it better
Original: https://hpecodewars.org/api/Files/checkProbPy.zip
Prints whether test case passed or failed, minor modifications to printing, and asks for file input number
Files should be formatted as f"prob{num}.py" num ranges from 0-99 and should always be zfilled 2 digits (ex. 04 not 4)
This program should be run on in a root directory (not in a sub-folder)
Running with incorrect folder organization will cause errors
This should have both windows and linux support (tested on Win10 and Github Codespaces)
If the location provided by sys.executable is not working, do the following:
Modify PY3_PATH to your path.
This can be found (in vs code) by clicking the version number in the bottom right while viewing a .py file
"""
import argparse
import glob
import os
import sys
import re
import subprocess
#JDK_PATH is not supported, keep empty
JDK_PATH = ""
#Unless not working as described above, these should not need to be editted.
PY3_PATH = sys.executable
# **DO NOT** edit these constants
STUDENT_DIR = "student_datasets"
INPUT_TEXT_FILE_NAME = "input.txt"
TERMINAL_WIDTH = 80
COMMON_HEADER_PREFIX = "Problem #{} - data set {}"
PROBLEM_OUTPUT_HEADER_TEMPLATE = COMMON_HEADER_PREFIX + " - Program output begins"
DATASET_HEADER_TEMPLATE = COMMON_HEADER_PREFIX + " - INPUT DATA"
EXPECTED_HEADER = "Compare to EXPECTED output below"
def get_wide_message(message, fill_char):
"""
Get a message aligned to the terminal width using a fill char on the left and right
Left padding is 4
:param message: Message
:param fill_char: Character to use as filler
:return: Wide message string
"""
left_padding = 4
spaces = 2
# Calculate the right padding based on the message length
right_padding = TERMINAL_WIDTH - left_padding - len(message) - spaces
# For long messages do not print any trailing filler
if right_padding < 0:
right_padding = 0
return "{} {} {}".format(fill_char * left_padding, message, fill_char * right_padding)
def get_header(message, header_char='-', bar_char='-'):
"""
Get a message with a nice header box using dashes and a blank line (unless newline is False)
:param message: The message to display
:param header_char: Character to use in the header
:param bar_char: Character to use in the bar
:return: Header message
"""
bar = bar_char * TERMINAL_WIDTH
return "{}\n{}\n{}\n".format(bar, get_wide_message(message, header_char), bar)
def display_error(message):
"""
Display an error message
:param message: Error message
:return:
"""
print("!! ERROR: {}".format(message))
def verify_paths():
"""
Verify that the paths for Java, and Python are valid
An empty path is valid and implies that the path will not be used.
:return: True if all paths are valid, false otherwise
"""
valid = True
# JDK path must be a directory (bin) so we can use javac and java
if JDK_PATH and (not os.path.isdir(JDK_PATH) or os.path.basename(JDK_PATH) != "bin"):
display_error("JDK_PATH was set to `{}` which is NOT the 'bin' directory of the JDK.".format(JDK_PATH))
valid = False
if PY3_PATH and not os.path.isfile(PY3_PATH):
display_error("PY3_PATH was set to `{}` which is NOT a file (python executable)".format(PY3_PATH))
valid = False
if not os.path.isdir(STUDENT_DIR):
display_error("{} is missing! Where are the data sets?.".format(STUDENT_DIR))
valid = False
if not valid:
print("\n--> Remember: If you are not using one of the languages, set the path to the empty string (\"\")")
return valid
def check(solution_file_path, show_input):
"""
Check a problem
:param solution_file_path: Filename for the solution to be checked
:param show_input: Show the input data file
:return:
"""
problem_number, file_type = determine_problem_number(solution_file_path)
if problem_number:
print(get_header("Checking Problem {} ".format(problem_number), header_char='*', bar_char='='))
run_command = pre_process_source(problem_number, file_type, solution_file_path)
if not run_command or len(run_command) == 0:
display_error("Failed to pre-process source file!")
return
check_problem(problem_number, run_command, show_input)
print(get_header("Checking COMPLETE", header_char='*', bar_char='='))
else:
display_error("Skipping {} because it is not a valid solution file.".format(solution_file_path))
def determine_problem_number(solution_file_path):
"""
Check the local directory for a file matching probXY and return the problem number
:param solution_file_path: File name path for the solution
:return: problem number, suffix
"""
problem_number = None
file_type = None
# Our pattern works on the base name of the file
file_name = os.path.basename(solution_file_path)
pattern = r"prob(\d{2}).(java|py|c|cpp)$"
match = re.match(pattern, file_name)
if match:
problem_number, file_type = match.group(1, 2)
return problem_number, file_type
def pre_process_source(problem_number, file_type, filename):
"""
Perform any necessary pre-process steps for this source file
:param problem_number: Problem number
:param file_type: Type of file
:param filename: Filename path
:return:
"""
run_command = None
java_command = os.path.join(JDK_PATH, "java")
problem_exe_name = "prob{}.exe".format(problem_number)
if file_type == "java":
# Only build the run command if the compile was a success
javac_path = os.path.join(JDK_PATH, "javac")
if compile_from_source([javac_path, filename], problem_number, filename, file_type):
run_command = [java_command, "-cp", ".", "prob{}".format(problem_number)]
elif file_type == "py":
run_command = [PY3_PATH, filename]
elif file_type in ["c", "cpp"]:
# Select the correct compiler
compiler = "gcc" if file_type == "c" else "g++"
compile_command = [compiler, "-o", problem_exe_name, filename, "-lm"]
# Only build the run command if the compile was a success
if compile_from_source(compile_command, problem_number, filename, file_type):
run_command = [os.path.join('.', problem_exe_name)]
return run_command
def compile_from_source(compile_command, problem_number, filename, file_type):
"""
Compile the from source
:param compile_command: Command used to compile the soure file
:param problem_number: Problem number
:param filename: Filename path
:param file_type: File type used for the message
:return: True if the pre-processing succeeded
"""
print("Compile {} source file... ".format(file_type))
result = subprocess.run(compile_command)
if result.returncode != 0:
display_error("The {} source file {} did not compile for problem {}".format(file_type, filename, problem_number))
return False
print("Done")
return True
def check_problem(problem_number, run_command, show_input_file):
"""
Check the given problem against all data sets
:param problem_number: Problem number
:param run_command: Command to run the solution
:param show_input_file: Show the input data file
:return:
"""
# File name pattern
file_pattern = r"prob{}-{}-{}.txt"
# Track if we find any dataset files
found_dataset = False
for input_file in sorted(os.listdir(STUDENT_DIR)):
# Pass the (\d) regex group pattern for the data set number
match = re.match(file_pattern.format(problem_number, r"(\d)", "in"), input_file)
if not match:
continue
found_dataset = True
dataset_number = match.group(1)
# Get the current data set file contents
dataset_input_file = os.path.join(STUDENT_DIR, file_pattern.format(problem_number, dataset_number, "in"))
with open(dataset_input_file, "r") as f:
lines = f.readlines()
if show_input_file:
# Show the dataset input file
print(get_header(DATASET_HEADER_TEMPLATE.format(problem_number, dataset_number)))
print("".join(lines))
# Write the current dataset to input.txt
with open(INPUT_TEXT_FILE_NAME, "w") as out:
out.writelines("".join(lines))
print(get_header(PROBLEM_OUTPUT_HEADER_TEMPLATE.format(problem_number, dataset_number)))
with open(INPUT_TEXT_FILE_NAME, 'r') as in_file:
result = subprocess.run(run_command, stdin=in_file, capture_output=True)
formatted_stdout = result.stdout.decode().replace("\r\n", "\n")
print(formatted_stdout)
if result and result.returncode != 0:
display_error("Unable to run solution, return code was: {}".format(result.returncode))
print(get_header(EXPECTED_HEADER.format(problem_number, dataset_number)))
# Show the expected dataset output file
dataset_output_file = os.path.join(STUDENT_DIR, file_pattern.format(problem_number, dataset_number, "out"))
with open(dataset_output_file, "r") as f:
expected_lines = f.readlines()
print("".join(expected_lines))
formatted_expected = "".join(expected_lines).replace("\r\n", "\n")
if formatted_expected == formatted_stdout:
print("\033[0;32m" + "Passed!" + "\033[0m")
else:
print("\033[0;31m" + "Failed!" + "\033[0m")
# Prompt for next dataset
prompt_text = "{} {} {}".format('-' * 10, "Press ENTER to continue check next data set",
'-' * 10)
input(prompt_text)
if not found_dataset:
display_error("No dataset file found for problem {} !".format(problem_number))
def parse_arguments():
"""
Define and parse the arguments to the script
:return: args parsed by the argument parser
"""
parser = argparse.ArgumentParser(description="CodeWars Check Problem script. "
"The problem number is automatically determined from the filename. "
"If there are multiple solution files it will check them one-by-one.")
parser.add_argument("-i", "--show-input",
help="Show the input file",
action="store_true",
dest="show_input")
parser.add_argument("--version",
help="Program version",
action="version",
version="%(prog)s {}".format(__version__))
return parser.parse_args()
def main():
print("CodeWars Check Problem Script BETA v{} -- Use with care --\n".format(__version__))
args = parse_arguments()
if not verify_paths():
return
glob_files = glob.glob(os.path.join('.', "prob*"))
"""
# Skip exe and class files
solution_files = [file_name for file_name in glob_files if 'exe' not in file_name and 'class' not in file_name]
if not solution_files or len(solution_files) != 1:
display_error("Ensure that EXACTLY one problem file is present in the current directory and try again.")
return
""" #dont do this either
problem_num = input('Problem Number: ')
solution_file = f"prob{problem_num.zfill(2)}.py"
check(os.path.basename(solution_file), args.show_input)
print()
# Cleanup
for local_file in sorted(os.listdir('.')):
if re.match(r"prob.*.class", local_file) or re.match(r"prob.*.exe", local_file):
print("Cleanup: Removing {}".format(local_file))
os.remove(local_file)
elif local_file == INPUT_TEXT_FILE_NAME:
#os.remove(local_file)
pass #don't do this
if __name__ == "__main__":
main()