def __init__(self): Tk.__init__(self) self.overrideredirect(1) # eliminate title bar ws = self.winfo_screenwidth() # width of the screen hs = self.winfo_screenheight() # height of the screen welcome_screen = ["Logo1_800.gif", "Logo2_800.gif"] path_welcome_screen = SrcPath.abs("gui", welcome_screen[randint(0, 1)]) background_image = PhotoImage(file=path_welcome_screen) win_width = 13.9 / 14.0 * background_image.width( ) # eliminates root bg win_height = 13.9 / 14.0 * background_image.height() # calculate x and y coordinates for the Tk root window x_position = (ws * 1.0 / 2.0) - (win_width * 1.0 / 2.0) y_position = (hs * 1.0 / 2.0) - (win_height * 1.0 / 2.0) background_label = Label(self, image=background_image) background_label.image = background_image # don't remove this line! # eliminates root bezels background_label.place(relx=0.5, rely=0.5, anchor=CENTER) self.geometry('%dx%d+%d+%d' % (win_width, win_height, x_position, y_position)) self.wait_visibility() self.lift() # place above all window self.after(2000, self.destroy)
def __init__(self, master): Frame.__init__(self, master) path_gears = SrcPath.abs("gui", "gears.gif") gears_image = PhotoImage(file=path_gears) gears_label = Label(self, image=gears_image) gears_label.image = gears_image # don't remove this line! gears_label.place(relx=0.5, rely=0.5, anchor=CENTER)
def test_cache_invalidation(self): # typical initialization config = Helper.read_conf(SrcPath.abs("config.yml")) db_name = os.path.join(self.data_path, "requests.db") db = UpdateRequestDB(db_name, config['gh_api_token'], self.DB_REPO_NAME) db.fetch_all(force_full_sync=True) self.assertGreater(len(db.requests), 7) # initialize with different repository with shelve.open(db_name) as cache: cache['repo'] = 'SOMETHING ELSE' db = UpdateRequestDB(db_name, config['gh_api_token'], self.DB_REPO_NAME) self.assertLess(len(db.requests), 7) db.fetch_all(force_full_sync=True) self.assertGreater(len(db.requests), 7) # initialize with corrupted data with shelve.open(db_name) as cache: cache['1'] = None cache['2'] = 'a string' cache['3'] = 123 cache['4'] = ['42', None] cache['5'] = 3.14 cache['6'] = {1, 2, 34} db = UpdateRequestDB(db_name, config['gh_api_token'], self.DB_REPO_NAME) self.assertLess(len(db.requests), 7)
def test_repo_manager(self): config_file = SrcPath.abs('config.yml') config = Helper.read_conf(config_file) url = ('https://%s:%[email protected]/teammask/hello-world.git' % (config['gh_username'], config['gh_api_token'])) repo_path = os.path.join(self.SHARED_PATH, "test_repo") repo = RepoManager(repo_path, url) self.assertIsNotNone(repo) self.assertTrue(repo.root == repo_path) self.assertTrue(path.isdir(repo.root)) self.assertTrue(repo.oec.description == RepoManager.REPO_BANNER) # Test commit: - Modify the readme file on master with open(os.path.join(repo_path, "README.md"), 'w') as readme_file: readme_file.write("Modified at: " + str(datetime.utcnow())) commit_hash = repo.commit("Test commit at: " + str(datetime.utcnow())) self.assertTrue(commit_hash != "") # forget last commit repo.oec.git.reset('--hard', 'HEAD~1') # Test branch and del_branch:- branch_result = repo.branch("ABCDE") self.assertTrue(branch_result == "ABCDE") self.assertTrue(repo.oec.active_branch.name == "ABCDE") self.assertTrue(repo.delete_branch("ABCDE", False)) for branch in repo.oec.branches: self.assertTrue(branch.name != "ABCDE") branch_result = repo.branch("ABCDEG") # Test push self.assertTrue(repo.push(branch_result)) found = False for branch in repo.oec.remote().refs: if branch.name == "origin/ABCDEG": found = True break self.assertTrue(found) # technically no branch was pushed if the commit reset was okay but # this line is here for good. repo.delete_branch("ABCDEG", True) # Test Pull. Any further testing is testing gitPython which is not # relevant self.assertTrue(repo.pull()) # Test Destroy repo.destroy() self.assertIsNone(repo.oec) self.assertTrue(repo.root == "") self.assertFalse(path.isdir(repo_path))
def test_get_similar(self): config = Helper.read_conf(SrcPath.abs("config.yml")) db_name = os.path.join(self.SHARED_PATH, "dbtest_requests.db") # initialize the db and fetch the latest requests db = UpdateRequestDB(db_name, config['gh_api_token'], self.DB_REPO_NAME) db.fetch_all(force_full_sync=False) # create a request that's the same as PR 1 duplicate_request = UpdateRequest( PlanetarySysUpdate( "HAT-P-55", planets=[ PlanetUpdate("HAT-P-55 b", fields={ "eccentricity": Quantity("0.139000"), "impactparameter": Quantity( "0.3920", error=("0.0860", "0.0730")), "period": Quantity( "3.58524670", unit='days', error=("0.00000640", "0.00000640") ) }) ]), title="Test request", message="message", reference="UpdateRequestDBTest" ) similar = db.get_similar(duplicate_request) self.assertIsNotNone(similar) self.assertEqual(1, similar.request.pullreq_num) # try submitting the request, # although it will always fail because we haven't pushed the branch # the db should still figure out it's a duplicate with self.assertRaises(DuplicateError): db.submit(duplicate_request) duplicate_request.updates.name = "HAT-P-42" similar = db.get_similar(duplicate_request) self.assertIsNone(similar)
def test_fetch(self): def verify_db( db_requests: Dict[str, Union[CachedRequest, IgnoredRequest]]): """Verifies the request db has the correct data.""" # 1 is valid and open request_1 = db_requests['1'].request self.assertEqual("Update HAT-P-55", request_1.title) self.assertEqual("HAT-P-55", request_1.updates.name) self.assertEqual(1, len(request_1.updates.planets)) self.assertEqual(3, len(request_1.updates.planets[0].fields)) self.assertEqual(1, request_1.pullreq_num) self.assertFalse(request_1.rejected) # 8 is invalid self.assertEqual(IgnoredRequest.invalid, db_requests['8']) # 4 has been merged self.assertEqual(IgnoredRequest.merged, db_requests['4']) # 7 is valid and closed request_7 = db_requests['7'].request self.assertEqual("HD 66428", request_7.updates.name) self.assertTrue(request_7.rejected) config = Helper.read_conf(SrcPath.abs("config.yml")) db_name = os.path.join(self.data_path, "requests.db") # initialize the db and fetch the latest requests db = UpdateRequestDB(db_name, config['gh_api_token'], self.DB_REPO_NAME) db.fetch_all(force_full_sync=True) request_ids = set(db.requests.keys()) self.assertGreater(len(request_ids), 1) verify_db(db.requests) # re-initialize the db without fetching db = UpdateRequestDB(db_name, config['gh_api_token'], self.DB_REPO_NAME) verify_db(db.requests)
def _reload_cat_config(self) -> None: """ Reload catalogue config files. """ del self.cats[:] # TODO: they will be fetched from a git repo in the future pattern = SrcPath.abs(self.config['cat_config_path'], '*.yml') for f in glob.glob(pattern): try: with open(f, 'r') as fin: cat_config = CatalogueConfig(fin) cat = MonitoredCatalogue(cat_config) self.cats.append(cat) except Exception as e: logging.error('Failed loading config "%s": %s' % (f, e)) if len(self.cats) == 0: logging.warning('No catalogue configurations found in "%s".' % pattern)
def fetch(self) -> None: """ Fetch the latest csv from monitored catalogue """ reader = None # debug feature - load file locally debug_file = self.config.raw.get('debug_file') if debug_file: debug_file = SrcPath.abs(debug_file) with open(debug_file, 'r') as f: # read the entire file so we can close it right away reader = csv.DictReader(StringIO(f.read())) logging.debug("Loaded catalogue from local debug file") if not reader: f = requests.get(self.config.url) result = f.text reader = csv.DictReader(StringIO(result)) # search the index of required field in raw field list # self._search_index(raw_col) # loop through each row in the csv self.systems = dict() def try_get_field(row: Dict[str, str], fielddesc: Dict[str, str], oec_field_name: str): cat_field_name = fielddesc.get(oec_field_name) if cat_field_name: row_value = row.get(cat_field_name) return row_value for next_planet in reader: system_name = next_planet[self.config.system_name] planet_name = None if self.config.planet_name: planet_name = next_planet[self.config.planet_name] else: planet_name = system_name + ' ' +\ next_planet[self.config.planet_letter] pl = Planet(planet_name, system_name) for oec_field, cat_fieldmeta in self.config.field_map.items(): value = next_planet.get(cat_fieldmeta['name']) if value: errorminus = try_get_field(next_planet, cat_fieldmeta, 'errorminus') errorplus = try_get_field(next_planet, cat_fieldmeta, 'errorplus') lowerlimit = try_get_field(next_planet, cat_fieldmeta, 'lowerlimit') upperlimit = try_get_field(next_planet, cat_fieldmeta, 'upperlimit') limitflag = try_get_field(next_planet, cat_fieldmeta, 'limit_flag') explicit_limit = lowerlimit or upperlimit errors = (lowerlimit, upperlimit)\ if explicit_limit else (errorminus, errorplus) q = Quantity( value, cat_fieldmeta.get('unit'), errors, bool(explicit_limit or (limitflag is None and bool(limitflag)))) # convert the quantity if it's using different unit oec_fieldmeta = oec.PLANET_FIELDS[oec_field] if oec_fieldmeta.unit != cat_fieldmeta.get('unit'): q = q.to(oec_fieldmeta.unit) pl.prop[oec_field] = q sys = self.systems.get(system_name) if sys is None: self.systems[system_name] = sys = System(system_name) sys.planets.append(pl)
class BaseTestCase(unittest.TestCase): # root folder containing tests TESTS_ROOT = SrcPath.abs('tests') # this folder will be ignored by git TEST_DATA = os.path.join(TESTS_ROOT, '.cache') # this folder contains private folders for each test run INDIVIDUAL_PATH = os.path.join(TEST_DATA, 'individual') # this folder contains everything that's shared among tests SHARED_PATH = os.path.join(TEST_DATA, 'shared') # private folders created for the last test run data_paths = [] # outcome of the last test run result = None def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.data_path = None @classmethod def setUpClass(cls): """ Injects our `setUp` or `tearDown` method to the inherited test case. """ if cls is not BaseTestCase: if cls.setUp is not BaseTestCase.setUp: orig_setup = cls.setUp def setup_override(self): """Overridden setup method""" BaseTestCase.setUp(self) return orig_setup(self) cls.setUp = setup_override # if cls.tearDown is not BaseTestCase.tearDown: # orig_teardown = cls.tearDown # # def teardown_override(self): # """Overridden teardown method""" # BaseTestCase.tearDown(self) # return orig_teardown(self) # cls.tearDown = teardown_override @classmethod def tearDownClass(cls): # clean up testing data if all tests passed if BaseTestCase.result and BaseTestCase.result.wasSuccessful(): for path in BaseTestCase.data_paths: try: shutil.rmtree(path, onerror=cls.__del_helper) except FileNotFoundError as e: pass except Exception as e: logging.exception(e) def run(self, result=None): # keep track of the latest test result if not result: result = self.defaultTestResult() BaseTestCase.result = result return unittest.TestCase.run(self, result) def setUp(self): # create shared folder if it doesn't exist os.makedirs(self.SHARED_PATH, exist_ok=True) # setup folder for the current test test_run = datetime.now().strftime("%Y%m%d_%H%M%S") + '_' + self.id() test_folder = os.path.join(BaseTestCase.INDIVIDUAL_PATH, test_run) os.makedirs(test_folder, exist_ok=True) self.data_path = test_folder if test_folder not in self.data_paths: self.data_paths.append(test_folder) @classmethod def __del_helper(cls, func, dir_path, exc): os.chmod(dir_path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) # 0777 func(dir_path) def clear_data_path(self): """ Clear application data path for running the current test case """ for file in os.listdir(self.data_path): fname = os.path.join(self.data_path, file) try: if os.path.isfile(fname): os.unlink(fname) elif os.path.isdir(fname): shutil.rmtree(fname, onerror=self.__del_helper) except Exception as e: print(e) ########################################################################### # Assertions ########################################################################### def assertPathEqual(self, path1, path2, msg=None): self.assertEqual(os.path.normpath(path1), os.path.normpath(path2), msg)
class Quantity: """ Representation of a quantity and associated error in a specific unit system. """ # Load unit registry from module resources _UR = UnitRegistry() _UR.load_definitions(SrcPath.abs('sync/resources/units.txt')) NUM_REGEX = re.compile(R'[-+]?\d*\.?\d+([eE][-+]?\d+)?') def __init__(self, value: str, unit: str = None, error: Tuple[str, str] = None, is_limit: bool = False): """ Construct a Quantity object. :param value: The quantity value. :param unit: The unit system. :param error: The lower bound and upper bound of error. :param is_limit: If set to True, the error tuple represents lower limit and upper limit. """ if not value: raise ValueError("Value cannot be empty: " + repr(value)) value = value.strip() if not self._is_num(value): raise ValueError("Invalid number: " + repr(value)) if error and not (self._is_num(error[0]) and self._is_num(error[1])): error = None # errors must be defined in a pair if error and not is_limit: # error-minus and error-plus pair should be non-negative error = (str(abs(Decimal(error[0]))), str(abs(Decimal(error[1])))) self.value = value self.unit = unit self.error = error self.is_limit = is_limit def __eq__(self, other: 'Quantity'): # keep the type quoted ^, class is incomplete at this point # value if Decimal(self.value) != Decimal(other.value): return False # unit if not self._eq_unit(self.unit, other.unit): return False return True def __repr__(self): return "Quantity(" + repr(self.value) + "," + repr(self.unit) + "," + \ repr(self.error) + "," + repr(self.is_limit) + ")" @classmethod def _is_num(cls, value: str): """ Returns whether a string value is a valid value :param value: a string representing a number """ return value is not None and bool(cls.NUM_REGEX.fullmatch(value)) @classmethod def _eq_unit(cls, a: str, b: str) -> bool: if (a is None) != (b is None): return False if a is not None and cls._UR.get_name(a) != cls._UR.get_name(b): return False return True def to(self, new_unit: str) -> 'Quantity': """ Convert to another unit system. :param new_unit: The target unit system. :return: A new quantity value using the target unit system """ def conv(original: str) -> str: """ Converts a value from current unit to the new_unit :param original: string representing a number :return: string representing converted number """ # Decimal type is not so well supported in pint package # but it preserves precisions and doesn't have numeric errors # # It fails if the conversion between two units involves an offset # e.g. Kelvin -> Celsius old = self._UR.Quantity(Decimal(original), self.unit) new = old.to(new_unit) return str(new.magnitude) return Quantity( conv(self.value), new_unit, None if not self.error else ( conv(self.error[0]), conv(self.error[1]), ), self.is_limit) def get_error_or_limit(self, is_limit: bool) -> Tuple[str, str]: """ Get the error/limit tuple: (errorminus, errorplus), or (lowerlimit, upperlimit) from this quantity. :param is_limit: return limit tuple? otherwise error tuple """ # conversion not need if not (self.error and is_limit != self.is_limit): return self.error if is_limit: # convert to limit/bound tuple return (str(Decimal(self.value) - Decimal(self.error[0])), str(Decimal(self.value) + Decimal(self.error[1]))) else: # convert to error tuple return (str(Decimal(self.value) - Decimal(self.error[0])), str(Decimal(self.error[1]) - Decimal(self.value))) def can_update_error(self, other: 'Quantity') -> bool: """ Returns whether the error term of this quantity can be updated from the other quantity. Assumes the two quantities already have the same value and unit, otherwise the return value makes no sense. :param other: the other quantity """ assert self._eq_unit(self.unit, other.unit) # the new quantity has no error term if not other.error: return False # this quantity has no error term, can always update if not self.error: return True # they have the same error term if self.error == other.error and self.is_limit == other.is_limit: return False # Cannot conclude yet, because # 1. ('0.0', '0.0') equals to ('0', '0') # 2. need conversion between errors and limits/bounds othererror = other.error if self.is_limit != other.is_limit: # conversion between errors and limits othererror = other.get_error_or_limit(self.is_limit) # compare the errors for i in range(2): if Decimal(self.error[i]) != Decimal(othererror[i]): return True return False