def validate_arguments() -> bool: def validate_parameterization_arg(i, arg) -> bool: try: light_param_def = get_parsed_light( light_name).light_param_def if (arg in light_param_def and list( get_overload_column_info( overload_type).values())[i]): return True except KeyError: return False else: if ((arg == "NOOP" or arg.startswith("sim/")) and i == len(light_args) - 1): return True elif re.match("-?\d+(\.\d+)?", arg): return True else: return False prev_logger_errors = len(logger.findErrors()) for i, arg in enumerate(light_args): if not validate_parameterization_arg(i, arg): logger.error( f"{line_num}, '{light_name}', arg #{i+1}: ('{arg}')" f" is not a correctly formatted number or is invalid" ) continue return not (len(logger.findErrors()) - prev_logger_errors)
def test_scenery_glass_illegal(self): filename = 'test_scenery_glass_illegal' xplaneFile = xplane_file.createFileFromBlenderLayerIndex(7) out = xplaneFile.write() self.assertEqual(len(logger.findErrors()), 1) logger.clearMessages()
def test_export(self): filename = 'test_IST_2Mats_VarSPEC' xplaneFile = xplane_file.createFileFromBlenderLayerIndex(0) out = xplaneFile.write() self.assertEquals(len(logger.findErrors()), self.expected_logger_errors)
def test_export(self): filename = 'test_SSO_IllegalUsePanelTex' xplaneFile = xplane_file.createFileFromBlenderLayerIndex(0) out = xplaneFile.write() self.assertEquals(len(logger.findErrors()), self.expected_logger_errors)
def test_export(self): filename = 'test_SSO_2incompatibleDraped_Mats' xplaneFile = xplane_file.createFileFromBlenderLayerIndex(0) out = xplaneFile.write() self.assertEquals(len(logger.findErrors()), self.expected_logger_errors)
def test_scenery_glass_illegal(self): filename = 'test_scenery_glass_illegal' xplaneFile = xplane_file.createFileFromBlenderLayerIndex(7) out = xplaneFile.write() self.assertEqual(len(logger.findErrors()), 1) logger.clearMessages()
def assertLoggerErrors(self, expected_logger_errors: int) -> None: """ Asserts the logger has some number of errors, then clears the logger of all messages """ try: found_errors = len(logger.findErrors()) self.assertEqual(found_errors, expected_logger_errors) except AssertionError as e: raise AssertionError( f"Expected {expected_logger_errors} logger errors, got {found_errors}" ) from None else: logger.clearMessages()
def assertLoggerErrors(self, expected_logger_errors): self.assertEqual(len(logger.findErrors()), expected_logger_errors) logger.clearMessages()
def test_no_material(self): xplaneFile = xplane_file.createFileFromBlenderLayerIndex(0) out = xplaneFile.write() self.assertEquals(len(logger.findErrors()), self.expected_logger_errors)
def test_norm_met_off_two_drap_scen(self): expected_logger_errors = 1 #"test_norm_met_off_two_drap_scen" out = xplane_file.createFileFromBlenderLayerIndex(3).write() self.assertEqual(len(logger.findErrors()), expected_logger_errors) logger.clearMessages()
def assertLoggerErrors(self, expected_logger_errors): self.assertEqual(len(logger.findErrors()), expected_logger_errors) logger.clearMessages()
def test_glass_2_mats_conflict(self): xplaneFile = xplane_file.createFileFromBlenderLayerIndex(8) out = xplaneFile.write() self.assertEqual(len(logger.findErrors()), 1) logger.clearMessages()
def test_scenery_glass_illegal(self): filename = 'test_scenery_glass_illegal' out = self.exportLayer(7) self.assertEqual(len(logger.findErrors()), 1) logger.clearMessages()
def test_export(self): xplaneFile = xplane_file.createFileFromBlenderLayerIndex(0) out = xplaneFile.write() self.assertEquals(len(logger.findErrors()), 1)
def test_norm_met_off_two_drap_scen(self): expected_logger_errors = 1 #"test_norm_met_off_two_drap_scen" out = xplane_file.createFileFromBlenderLayerIndex(3).write() self.assertEqual(len(logger.findErrors()), expected_logger_errors) logger.clearMessages()
def test_glass_2_mats_conflict(self): out = self.exportLayer(8) self.assertEqual(len(logger.findErrors()), 1) logger.clearMessages()
def test_glass_2_mats_conflict(self): xplaneFile = xplane_file.createFileFromBlenderLayerIndex(8) out = xplaneFile.write() self.assertEqual(len(logger.findErrors()), 1) logger.clearMessages()
def parse_lights_file(): """ Parse the lights.txt file, building the dictionary of parsed lights. If already parsed, does nothing. Raises OSError or ValueError if file not found or content invalid, logger errors and warnings will have been collected """ global _parsed_lights_txt_content if _parsed_lights_txt_content: return num_logger_problems = len(logger.findErrors()) LIGHTS_FILEPATH = os.path.join(xplane_constants.ADDON_RESOURCES_FOLDER, "lights.txt") if not os.path.isfile(LIGHTS_FILEPATH): logger.error( f"lights.txt file was not found in resource folder {LIGHTS_FILEPATH}" ) raise FileNotFoundError def is_allowed_param(p: str) -> bool: return (p in { "R", "G", "B", "A", "SIZE", "DX", "DY", "DZ", "WIDTH", "FREQ", "PHASE", "INDEX", "DIR_MAG", } or p.startswith(("UNUSED", "NEG_ONE", "ZERO", "ONE"))) with open(LIGHTS_FILEPATH, "r") as f: lines = [(line_num, l.strip()) for line_num, l in enumerate(f.read().splitlines()) if l.startswith((*OVERLOAD_TYPES, "LIGHT_PARAM_DEF"))] for line_num, line in lines: #print(line) try: overload_type, light_name, *light_args = line.split() if not light_args: raise ValueError except ValueError: # not enough values to unpack logger.error( f"{line_num}: Line could not be parsed to '<RECORD_TYPE> <light_name> <params or args list>'" ) continue if not re.match("[A-Za-z0-9_]+", light_name): logger.error( f"{line_num}: Light name '{light_name}' must be upper/lower case letters, numbers, or underscores only" ) continue def get_parsed_light_of_content_dict( light_name: str) -> ParsedLight: try: _parsed_lights_txt_content[light_name] except KeyError: _parsed_lights_txt_content[light_name] = ParsedLight( light_name) finally: return _parsed_lights_txt_content[light_name] if overload_type == "LIGHT_PARAM_DEF": parsed_light = get_parsed_light_of_content_dict(light_name) if parsed_light.light_param_def: logger.error( f"{line_num}: {light_name} cannot have more than one LIGHT_PARAM_DEF" ) continue light_argc, *light_argv = light_args try: light_argc = int(light_argc) except ValueError: logger.error( f"{line_num}: Parameter count for '{light_name}''s LIGHT_PARAM_DEF must be an int, is '{light_argc}'" ) continue else: if not light_argc or not light_argv or (light_argc != len(light_argv)): logger.error( f"{line_num}: '{light_name}''s LIGHT_PARAM_DEF must have a count > 0 and an parameter list of the same length" ) continue elif len(set(light_argv)) < len(light_argv): logger.error( f"{line_num}: '{light_name}''s LIGHT_PARAM_DEF has duplicate parameters in it" ) continue parsed_light.light_param_def = light_argv # Skip the count if parsed_light.light_param_def and any( not is_allowed_param(param) for param in parsed_light.light_param_def): logger.error( f"{line_num}: LIGHT_PARAM_DEF for '{light_name}' contains unknown or invalid parameters: {parsed_light.light_param_def}" ) continue elif overload_type not in OVERLOAD_TYPES: logger.error( f"{line_num}: '{overload_type}' is not a valid OVERLOAD_TYPE." ) continue elif len(light_args) < len( get_overload_column_info(overload_type)): logger.error( f"{line_num}: Arguments list for '{overload_type} {light_name} {' '.join(light_args)}' is not long enough" ) continue elif len(light_args) > len( get_overload_column_info(overload_type)): logger.error( f"{line_num}: Arguments list for '{overload_type} {light_name} {' '.join(light_args)}' is too long" ) continue else: parsed_light = get_parsed_light_of_content_dict(light_name) def validate_arguments() -> bool: def validate_parameterization_arg(i, arg) -> bool: try: light_param_def = get_parsed_light( light_name).light_param_def if (arg in light_param_def and list( get_overload_column_info( overload_type).values())[i]): return True except KeyError: return False else: if ((arg == "NOOP" or arg.startswith("sim/")) and i == len(light_args) - 1): return True elif re.match("-?\d+(\.\d+)?", arg): return True else: return False prev_logger_errors = len(logger.findErrors()) for i, arg in enumerate(light_args): if not validate_parameterization_arg(i, arg): logger.error( f"{line_num}, '{light_name}', arg #{i+1}: ('{arg}')" f" is not a correctly formatted number or is invalid" ) continue return not (len(logger.findErrors()) - prev_logger_errors) if not validate_arguments(): continue def tryfloat(s: str) -> float: try: return float(s) except ValueError: return s parsed_light.overloads.append( ParsedLightOverload(overload_type=overload_type, name=light_name, arguments=list( map(tryfloat, light_args)))) # This is a heuristic/careful reading of X-Plane's light system # of what is most likely to give us # the correct direction to autocorrect rankings = [ "SPILL_HW_DIR", # Most trustworthy "SPILL_HW_FLA", "SPILL_SW", "BILLBOARD_HW", "BILLBOARD_SW", # Least trustworthy "SPILL_GND", # Ignored by autocorrector, ranked last "SPILL_GND_REV", # Ignored by autocorrector, ranked last ] # Semantically speaking, overloads[0] must ALWAYS be the most trustworthy parsed_light.overloads.sort( key=lambda l: rankings.index(l.overload_type)) for light_name, pl in _parsed_lights_txt_content.items(): if not pl.overloads: logger.error( f"Ignoring '{light_name}': Found LIGHT_PARAM_DEF but no valid overloads" ) continue _parsed_lights_txt_content = { light_name: pl for light_name, pl in _parsed_lights_txt_content.items() if pl.overloads } if not _parsed_lights_txt_content: logger.error("lights.txt had no valid light records in it") if len(logger.findErrors()) - num_logger_problems: raise LightsTxtFileParsingError