|
| 1 | +## Copyright 2023 Intel Corporation |
| 2 | +## SPDX-License-Identifier: Apache-2.0 |
| 3 | + |
| 4 | +import subprocess |
| 5 | +import os |
| 6 | +import platform |
| 7 | +import sys |
| 8 | +import signal |
| 9 | +from threading import Timer |
| 10 | + |
| 11 | +# Flag for enabling additional verbose debug info |
| 12 | +debug_enabled = True |
| 13 | + |
| 14 | +# Representation of output which goes to console. |
| 15 | +# It consist of stdout & stderr. |
| 16 | +class Output(object): |
| 17 | + stdout: str |
| 18 | + stderr: str |
| 19 | + def __init__(self, stdout:str, stderr:str): |
| 20 | + self.stdout = stdout |
| 21 | + self.stderr = stderr |
| 22 | + |
| 23 | + # By default we can get string from instance of this class |
| 24 | + # which will return merged stdout with stderr. |
| 25 | + def __str__(self): |
| 26 | + return self.stdout + self.stderr |
| 27 | + |
| 28 | +class TestCommandTool: |
| 29 | + def run(self, cmd, timeout, cwd = os.getcwd(), test_env = os.environ.copy(), print_output = True): |
| 30 | + exit_code = 0 |
| 31 | + proc = subprocess.Popen(cmd, shell=True, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=None, universal_newlines=True, env=test_env) |
| 32 | + |
| 33 | + # This variable we use to pass value back from |
| 34 | + # Timer callback function - _kill. |
| 35 | + timeout_flag_wrapper = {'timeout_occured': False} |
| 36 | + |
| 37 | + # We use timer to not hang forever on |
| 38 | + # proc.communicate(), in case of timeout |
| 39 | + # _kill method will kill process and |
| 40 | + # it will return from proc.communicate() |
| 41 | + # immediately. |
| 42 | + timer = Timer(timeout, self._kill, [proc, timeout_flag_wrapper], {}) |
| 43 | + timer.start() |
| 44 | + stdout, stderr = proc.communicate() |
| 45 | + timer.cancel() |
| 46 | + exit_code = proc.poll() |
| 47 | + |
| 48 | + if timeout_flag_wrapper["timeout_occured"]: |
| 49 | + exit_code = 124 |
| 50 | + stderr += "=K=> Timeout expired, process was killed." |
| 51 | + |
| 52 | + output = Output(stdout, stderr) |
| 53 | + if print_output: |
| 54 | + print(output, flush=True) |
| 55 | + |
| 56 | + return (exit_code, output) |
| 57 | + |
| 58 | + def _kill(self, proc_to_kill, timeout_flag_wrapper): |
| 59 | + timeout_flag_wrapper["timeout_occured"] = True |
| 60 | + pid = proc_to_kill.pid |
| 61 | + # Windows |
| 62 | + if platform.system() == 'Windows': |
| 63 | + proc = subprocess.Popen(['taskkill', '/F', '/T', '/PID', str(pid)], shell=True) |
| 64 | + proc.wait() |
| 65 | + # Linux |
| 66 | + else: |
| 67 | + proc = subprocess.Popen('pkill -TERM -P '+ str(pid), shell=True) |
| 68 | + proc.wait() |
| 69 | + |
| 70 | +class OpenVKLTestCase: |
| 71 | + renderer:str = None |
| 72 | + volume_type:str = None |
| 73 | + max_mse:float = None |
| 74 | + spp:int = None |
| 75 | + extra_gpu_args:str = None |
| 76 | + |
| 77 | + __gpu_exit_code:int = None |
| 78 | + __gpu_output:Output = None |
| 79 | + |
| 80 | + __cpu_exit_code:int = None |
| 81 | + __cpu_output:Output = None |
| 82 | + |
| 83 | + __diff_exit_code:int = None |
| 84 | + __diff_output:Output = None |
| 85 | + |
| 86 | + def __init__(self, renderer : str, volume_type : str, extra_gpu_args : str = ''): |
| 87 | + self.renderer = renderer |
| 88 | + self.volume_type = volume_type |
| 89 | + self.max_mse = 0.000001 |
| 90 | + self.spp = 2 |
| 91 | + self.extra_gpu_args = extra_gpu_args |
| 92 | + |
| 93 | + # For this particular case we need to set higher MSE treshold |
| 94 | + if renderer == "hit_iterator_renderer" and volume_type == "structuredRegular": |
| 95 | + self.max_mse = 0.000015 |
| 96 | + |
| 97 | + # For density pathtracer we want more spp to get picutre closer to final image |
| 98 | + if renderer == "density_pathtracer": |
| 99 | + self.spp = 50 |
| 100 | + |
| 101 | + def __print_debug(self, msg:str): |
| 102 | + if debug_enabled: |
| 103 | + print(msg) |
| 104 | + |
| 105 | + def __get_example_cpu_binary_string(self) -> str: |
| 106 | + if platform.system() == 'Windows': |
| 107 | + return "vklExamplesCPU.exe" |
| 108 | + else: |
| 109 | + return "./vklExamplesCPU" |
| 110 | + |
| 111 | + def __get_example_gpu_binary_string(self) -> str: |
| 112 | + if platform.system() == 'Windows': |
| 113 | + return "vklExamplesGPU.exe" |
| 114 | + else: |
| 115 | + return "./vklExamplesGPU" |
| 116 | + |
| 117 | + def __get_common_params(self) -> str: |
| 118 | + return "-batch -framebufferSize 1024 1024" |
| 119 | + |
| 120 | + def get_name(self) -> str: |
| 121 | + return "%s-%s%s" % (self.renderer, self.volume_type, self.extra_gpu_args) |
| 122 | + |
| 123 | + def get_result(self) -> int: |
| 124 | + return self.__gpu_exit_code + self.__cpu_exit_code + self.__diff_exit_code |
| 125 | + |
| 126 | + def print_error_outputs(self): |
| 127 | + # Print output only for commands where exit code != 0 |
| 128 | + if self.__gpu_exit_code != 0: |
| 129 | + print("#!# GPU cmd output:") |
| 130 | + print(self.__gpu_output) |
| 131 | + |
| 132 | + if self.__cpu_exit_code != 0: |
| 133 | + print("#!# CPU cmd output:") |
| 134 | + print(self.__cpu_output) |
| 135 | + |
| 136 | + if self.__diff_exit_code != 0: |
| 137 | + print("#!# DIFF cmd output:") |
| 138 | + print(self.__diff_output) |
| 139 | + |
| 140 | + def get_MSE(self) -> float: |
| 141 | + stdout = self.__diff_output.stdout |
| 142 | + # MSE can't be negative and this is how we're returning error |
| 143 | + if (len(stdout) == 0) or (":" not in stdout): |
| 144 | + return -1.0 |
| 145 | + return float(stdout.splitlines()[0].split(": ")[1]) |
| 146 | + |
| 147 | + def execute(self, img_diff_tool_path:str): |
| 148 | + # Default timeout - 60 secs for each command |
| 149 | + timeout = 60 |
| 150 | + |
| 151 | + # Execute GPU example |
| 152 | + gpu_run_cmd = "%s -renderer %s_gpu %s -volumeType %s -spp %d %s" % (self.__get_example_gpu_binary_string(), self.renderer, self.__get_common_params(), self.volume_type, self.spp, self.extra_gpu_args) |
| 153 | + self.__print_debug("## Executing: '%s', with timeout: %d" % (gpu_run_cmd, timeout)) |
| 154 | + self.__gpu_exit_code, self.__gpu_output = TestCommandTool().run(gpu_run_cmd, timeout) |
| 155 | + |
| 156 | + # Execute CPU example |
| 157 | + cpu_run_cmd = "%s -renderer %s %s -volumeType %s -spp %d" % (self.__get_example_cpu_binary_string(), self.renderer, self.__get_common_params(), self.volume_type, self.spp) |
| 158 | + self.__print_debug("## Executing: '%s', with timeout: %d" % (cpu_run_cmd, timeout)) |
| 159 | + self.__cpu_exit_code, self.__cpu_output = TestCommandTool().run(cpu_run_cmd, timeout) |
| 160 | + |
| 161 | + # Rename generated images to new name pattern "renderer-volume_type" instead of "renderer" |
| 162 | + # so all images can be stored in the same directory. That way we can avoid overriding output image |
| 163 | + # by different volume types executions. |
| 164 | + src_gpu_file_path = os.path.join(os.getcwd(), "%s_gpu.pfm" % self.renderer) |
| 165 | + dst_gpu_file_path = os.path.join(os.getcwd(), "%s-gpu.pfm" % (self.get_name())) |
| 166 | + os.rename(src_gpu_file_path, dst_gpu_file_path) |
| 167 | + |
| 168 | + src_cpu_file_path = os.path.join(os.getcwd(), "%s.pfm" % self.renderer) |
| 169 | + dst_cpu_file_path = os.path.join(os.getcwd(), "%s-cpu.pfm" % (self.get_name())) |
| 170 | + os.rename(src_cpu_file_path, dst_cpu_file_path) |
| 171 | + |
| 172 | + # Calculate difference between GPU & CPU generated image |
| 173 | + img_diff_cmd = "%s %s %s %.10f" % (img_diff_tool_path, dst_gpu_file_path, dst_cpu_file_path, self.max_mse) |
| 174 | + self.__print_debug("## Executing: '%s', with timeout: %d" % (img_diff_cmd, timeout)) |
| 175 | + self.__diff_exit_code, self.__diff_output = TestCommandTool().run(img_diff_cmd, timeout) |
| 176 | + |
| 177 | + self.__print_debug("## MSE: %.10f" % self.get_MSE()) |
| 178 | + self.__print_debug("## Exit codes: %d %d %d" % (self.__gpu_exit_code, self.__cpu_exit_code, self.__diff_exit_code)) |
| 179 | + |
| 180 | +def main(): |
| 181 | + if len(sys.argv) <= 1: |
| 182 | + print("#!## [ERROR] First argument must contain path to diff_tool"); |
| 183 | + return 2 |
| 184 | + |
| 185 | + img_diff_tool_path = sys.argv[1] |
| 186 | + test_cases = [] |
| 187 | + |
| 188 | + # generate test cases |
| 189 | + renderer_list = ["density_pathtracer", "ray_march_iterator", "interval_iterator_debug", "hit_iterator_renderer"] |
| 190 | + volume_type_list = ["structuredRegular", "structuredSpherical", "unstructured", "particle", "amr", "vdb"] |
| 191 | + for renderer in renderer_list: |
| 192 | + for volume_type in volume_type_list: |
| 193 | + test_cases.append(OpenVKLTestCase(renderer, volume_type)) |
| 194 | + |
| 195 | + if volume_type == "structuredRegular": |
| 196 | + test_cases.append(OpenVKLTestCase(renderer, volume_type, "-deviceOnlySharedBuffers")) |
| 197 | + |
| 198 | + # execute test cases |
| 199 | + for test_case in test_cases: |
| 200 | + test_case.execute(img_diff_tool_path) |
| 201 | + |
| 202 | + # print summary & analyze results |
| 203 | + print() |
| 204 | + print("######################################### SUMMARY ##########################################") |
| 205 | + print() |
| 206 | + |
| 207 | + failed_test_cases = [] |
| 208 | + # For any more advanced table formatting external library should be used |
| 209 | + # or external class should be created. |
| 210 | + fixed_width_row_format = "%-5s %-7s %-65s %s" |
| 211 | + print(fixed_width_row_format % ("####", "Result", "Test case name", "MSE value")) |
| 212 | + print("--------------------------------------------------------------------------------------------") |
| 213 | + for test_case in test_cases: |
| 214 | + result = test_case.get_result() |
| 215 | + if result != 0: |
| 216 | + log_prefix = "#!##" |
| 217 | + result_str = "[FAIL]" |
| 218 | + failed_test_cases.append(test_case) |
| 219 | + else: |
| 220 | + log_prefix = "####" |
| 221 | + result_str = "[PASS]" |
| 222 | + print(fixed_width_row_format % (log_prefix, result_str, test_case.get_name(), "%.10f" % test_case.get_MSE())) |
| 223 | + |
| 224 | + total_count = len(test_cases) |
| 225 | + fail_count = len(failed_test_cases) |
| 226 | + pass_count = total_count - fail_count |
| 227 | + |
| 228 | + print() |
| 229 | + if fail_count == 0: |
| 230 | + print("#### All test cases PASSED (passrate: %d/%d)" % (pass_count, total_count)) |
| 231 | + return 0 |
| 232 | + |
| 233 | + print("#!## Some test cases FAILED (passrate: %d/%d)" % (pass_count, total_count)) |
| 234 | + print() |
| 235 | + # Print output from failed tests |
| 236 | + for test_case in failed_test_cases: |
| 237 | + print("#!## '%s' failure details:" % test_case.get_name()) |
| 238 | + test_case.print_error_outputs() |
| 239 | + return 1 |
| 240 | + |
| 241 | +if __name__ == "__main__": |
| 242 | + sys.exit(main()) |
0 commit comments