def snapshot(model, filename, pytest_args, exclusive, skip, solver, solver_timeout, experimental, custom_tests, custom_config): """ Take a snapshot of a model's state and generate a report. MODEL: Path to model file. Can also be supplied via the environment variable MEMOTE_MODEL or configured in 'setup.cfg' or 'memote.ini'. """ model_obj, sbml_ver, notifications = api.validate_model(model) if model_obj is None: LOGGER.critical( "The model could not be loaded due to the following SBML errors.") utils.stdout_notifications(notifications) api.validation_report(model, notifications, filename) sys.exit(1) if not any(a.startswith("--tb") for a in pytest_args): pytest_args = ["--tb", "no"] + pytest_args # Add further directories to search for tests. pytest_args.extend(custom_tests) config = ReportConfiguration.load() # Update the default test configuration with custom ones (if any). for custom in custom_config: config.merge(ReportConfiguration.load(custom)) model_obj.solver = solver _, results = api.test_model(model_obj, sbml_version=sbml_ver, results=True, pytest_args=pytest_args, skip=skip, exclusive=exclusive, experimental=experimental, solver_timeout=solver_timeout) with open(filename, "w", encoding="utf-8") as file_handle: LOGGER.info("Writing snapshot report to '%s'.", filename) file_handle.write(api.snapshot_report(results, config))
def history(model, message, rewrite, solver, solver_timeout, location, pytest_args, deployment, commits, skip, exclusive, experimental=None): # noqa: D301 """ Re-compute test results for the git branch history. MODEL is the path to the model file. MESSAGE is a commit message in case results were modified or added. [COMMIT] ... It is possible to list out individual commits that should be re-computed or supply a range <oldest commit>..<newest commit>, for example, memote history model.xml "chore: re-compute history" 6b84d05..cd49c85 There are two distinct modes: \b 1. Completely re-compute test results for each commit in the git history. This should only be necessary when memote is first used with existing model repositories. 2. By giving memote specific commit hashes, it will re-compute test results for those only. This can also be achieved by supplying a commit range. """ # callbacks.validate_path(model) callbacks.git_installed() if location is None: raise click.BadParameter("No 'location' given or configured.") if "--tb" not in pytest_args: pytest_args = ["--tb", "no"] + pytest_args try: LOGGER.info("Identifying git repository!") repo = git.Repo() except git.InvalidGitRepositoryError: LOGGER.critical( "The history requires a git repository in order to follow " "the model's commit history.") sys.exit(1) else: LOGGER.info("Success!") previous = repo.active_branch LOGGER.info("Checking out deployment branch {}.".format(deployment)) repo.git.checkout(deployment) # Temporarily move the results to a new location so that they are # available while checking out the various commits. engine = None tmp_location = mkdtemp() try: # Test if the location can be opened as a database. engine = create_engine(location) engine.dispose() new_location = location if location.startswith("sqlite"): # Copy the SQLite database to a temporary location. Other # databases are not file-based and thus git independent. url = location.split("/", maxsplit=3) if isfile(url[3]): copy2(url[3], tmp_location) new_location = "{}/{}".format("/".join(url[:3] + [tmp_location]), url[3]) LOGGER.info("Temporarily moving database from '%s' to '%s'.", url[3], join(tmp_location, url[3])) manager = SQLResultManager(repository=repo, location=new_location) except (AttributeError, ArgumentError): LOGGER.info("Temporarily moving results from '%s' to '%s'.", location, tmp_location) move(location, tmp_location) new_location = join(tmp_location, location) manager = RepoResultManager(repository=repo, location=new_location) LOGGER.info("Recomputing result history!") history = HistoryManager(repository=repo, manager=manager) history.load_history(model, skip={deployment}) if len(commits) == 0: commits = list(history.iter_commits()) elif len(commits) == 1 and ".." in commits[0]: commits = repo.git.rev_list(commits[0]).split(os.linesep) for commit in commits: cmt = repo.commit(commit) # Rewrite to full length hexsha. commit = cmt.hexsha if not is_modified(model, cmt): LOGGER.info("The model was not modified in commit '{}'. " "Skipping.".format(commit)) continue # Should we overwrite an existing result? if commit in history and not rewrite: LOGGER.info( "Result for commit '{}' exists. Skipping.".format(commit)) continue LOGGER.info("Running the test suite for commit '{}'.".format(commit)) blob = cmt.tree[model] model_obj, sbml_ver, notifications = _model_from_stream( blob.data_stream, blob.name) if model_obj is None: LOGGER.critical("The model could not be loaded due to the " "following SBML errors.") stdout_notifications(notifications) continue proc = Process(target=_test_history, args=(model_obj, sbml_ver, solver, solver_timeout, manager, commit, pytest_args, skip, exclusive, experimental)) proc.start() proc.join() LOGGER.info("Finished recomputing!") # Copy back all new and modified files and add them to the index. LOGGER.info("Committing recomputed results!") repo.git.checkout(deployment) if engine is not None: manager.session.close() if location.startswith("sqlite"): copy2(join(tmp_location, url[3]), url[3]) else: move(new_location, os.getcwd()) repo.git.add(".") repo.git.commit("--message", message) LOGGER.info("Success!") # Checkout the original branch. previous.checkout() LOGGER.info("Done.")
def run(model, collect, filename, location, ignore_git, pytest_args, exclusive, skip, solver, solver_timeout, experimental, custom_tests, deployment, skip_unchanged): """ Run the test suite on a single model and collect results. MODEL: Path to model file. Can also be supplied via the environment variable MEMOTE_MODEL or configured in 'setup.cfg' or 'memote.ini'. """ def is_verbose(arg): return (arg.startswith("--verbosity") or arg.startswith("-v") or arg.startswith("--verbose") or arg.startswith("-q") or arg.startswith("--quiet")) if ignore_git: repo = None else: callbacks.git_installed() repo = callbacks.probe_git() if collect: if repo is not None: if location is None: LOGGER.critical( "Working with a repository requires a storage location.") sys.exit(1) if not any(a.startswith("--tb") for a in pytest_args): pytest_args = ["--tb", "short"] + pytest_args if not any(is_verbose(a) for a in pytest_args): pytest_args.append("-vv") # Check if the model was changed in this commit. Exit `memote run` if this # was not the case. if skip_unchanged and repo is not None: commit = repo.head.commit if not is_modified(model, commit): LOGGER.info("The model was not modified in commit '%s'. Skipping.", commit.hexsha) sys.exit(0) # Add further directories to search for tests. pytest_args.extend(custom_tests) # Check if the model can be loaded at all. model, sbml_ver, notifications = api.validate_model(model) if model is None: LOGGER.critical( "The model could not be loaded due to the following SBML errors.") stdout_notifications(notifications) sys.exit(1) model.solver = solver code, result = api.test_model(model=model, sbml_version=sbml_ver, results=True, pytest_args=pytest_args, skip=skip, exclusive=exclusive, experimental=experimental, solver_timeout=solver_timeout) if collect: if repo is None: manager = ResultManager() manager.store(result, filename=filename) else: LOGGER.info("Checking out deployment branch.") # If the repo HEAD is pointing to the most recent branch then # GitPython's `repo.active_branch` works. Yet, if the repo is in # detached HEAD state, i.e., when a user has checked out a specific # commit as opposed to a branch, this won't work and throw a # `TypeError`, which we are circumventing below. try: previous = repo.active_branch previous_cmt = previous.commit is_branch = True except TypeError: previous_cmt = repo.head.commit is_branch = False repo.git.checkout(deployment) try: manager = SQLResultManager(repository=repo, location=location) except (AttributeError, ArgumentError): manager = RepoResultManager(repository=repo, location=location) LOGGER.info( "Committing result and changing back to working branch.") manager.store(result, commit=previous_cmt.hexsha) repo.git.add(".") repo.git.commit( "--message", "chore: add result for {}".format(previous_cmt.hexsha)) if is_branch: previous.checkout() else: repo.commit(previous_cmt)
def history(model, message, rewrite, solver, location, pytest_args, deployment, commits, skip, exclusive, experimental=None): # noqa: D301 """ Re-compute test results for the git branch history. MODEL is the path to the model file. MESSAGE is a commit message in case results were modified or added. [COMMIT] ... It is possible to list out individual commits that should be re-computed or supply a range <oldest commit>..<newest commit>, for example, memote history model.xml "chore: re-compute history" 6b84d05..cd49c85 There are two distinct modes: \b 1. Completely re-compute test results for each commit in the git history. This should only be necessary when memote is first used with existing model repositories. 2. By giving memote specific commit hashes, it will re-compute test results for those only. This can also be achieved by supplying a commit range. """ # callbacks.validate_path(model) callbacks.git_installed() if location is None: raise click.BadParameter("No 'location' given or configured.") if "--tb" not in pytest_args: pytest_args = ["--tb", "no"] + pytest_args try: LOGGER.info("Identifying git repository!") repo = git.Repo() except git.InvalidGitRepositoryError: LOGGER.critical( "The history requires a git repository in order to follow " "the model's commit history.") sys.exit(1) else: LOGGER.info("Success!") previous = repo.active_branch LOGGER.info("Checking out deployment branch {}.".format(deployment)) repo.git.checkout(deployment) # Temporarily move the results to a new location so that they are # available while checking out the various commits. engine = None tmp_location = mkdtemp() try: # Test if the location can be opened as a database. engine = create_engine(location) engine.dispose() new_location = location if location.startswith("sqlite"): # Copy the SQLite database to a temporary location. Other # databases are not file-based and thus git independent. url = location.split("/", maxsplit=3) if isfile(url[3]): copy2(url[3], tmp_location) new_location = "{}/{}".format( "/".join(url[:3] + [tmp_location]), url[3]) LOGGER.info("Temporarily moving database from '%s' to '%s'.", url[3], join(tmp_location, url[3])) manager = SQLResultManager(repository=repo, location=new_location) except (AttributeError, ArgumentError): LOGGER.info("Temporarily moving results from '%s' to '%s'.", location, tmp_location) move(location, tmp_location) new_location = join(tmp_location, location) manager = RepoResultManager(repository=repo, location=new_location) LOGGER.info("Recomputing result history!") history = HistoryManager(repository=repo, manager=manager) history.load_history(model, skip={deployment}) if len(commits) == 0: commits = list(history.iter_commits()) elif len(commits) == 1 and ".." in commits[0]: commits = repo.git.rev_list(commits[0]).split(os.linesep) for commit in commits: cmt = repo.commit(commit) # Rewrite to full length hexsha. commit = cmt.hexsha if not is_modified(model, cmt): LOGGER.info( "The model was not modified in commit '{}'. " "Skipping.".format(commit)) continue # Should we overwrite an existing result? if commit in history and not rewrite: LOGGER.info( "Result for commit '{}' exists. Skipping.".format(commit)) continue LOGGER.info( "Running the test suite for commit '{}'.".format(commit)) blob = cmt.tree[model] model_obj, sbml_ver, notifications = _model_from_stream( blob.data_stream, blob.name ) if model_obj is None: LOGGER.critical("The model could not be loaded due to the " "following SBML errors.") stdout_notifications(notifications) continue proc = Process( target=_test_history, args=(model_obj, sbml_ver, solver, manager, commit, pytest_args, skip, exclusive, experimental)) proc.start() proc.join() LOGGER.info("Finished recomputing!") # Copy back all new and modified files and add them to the index. LOGGER.info("Committing recomputed results!") repo.git.checkout(deployment) if engine is not None: manager.session.close() if location.startswith("sqlite"): copy2(join(tmp_location, url[3]), url[3]) else: move(new_location, os.getcwd()) repo.git.add(".") check_call(['git', 'commit', '-m', message]) LOGGER.info("Success!") # Checkout the original branch. previous.checkout() LOGGER.info("Done.")
def run(model, collect, filename, location, ignore_git, pytest_args, exclusive, skip, solver, experimental, custom_tests, deployment, skip_unchanged): """ Run the test suite on a single model and collect results. MODEL: Path to model file. Can also be supplied via the environment variable MEMOTE_MODEL or configured in 'setup.cfg' or 'memote.ini'. """ def is_verbose(arg): return (arg.startswith("--verbosity") or arg.startswith("-v") or arg.startswith("--verbose") or arg.startswith("-q") or arg.startswith("--quiet")) if ignore_git: repo = None else: callbacks.git_installed() repo = callbacks.probe_git() if collect: if repo is not None: if location is None: LOGGER.critical( "Working with a repository requires a storage location.") sys.exit(1) if not any(a.startswith("--tb") for a in pytest_args): pytest_args = ["--tb", "short"] + pytest_args if not any(is_verbose(a) for a in pytest_args): pytest_args.append("-vv") # Check if the model was changed in this commit. Exit `memote run` if this # was not the case. if skip_unchanged and repo is not None: commit = repo.head.commit if not is_modified(model, commit): LOGGER.info("The model was not modified in commit '%s'. Skipping.", commit.hexsha) sys.exit(0) # Add further directories to search for tests. pytest_args.extend(custom_tests) # Check if the model can be loaded at all. model, sbml_ver, notifications = api.validate_model(model) if model is None: LOGGER.critical( "The model could not be loaded due to the following SBML errors.") stdout_notifications(notifications) sys.exit(1) model.solver = solver # Load the experimental configuration using model information. if experimental is not None: experimental.load(model) code, result = api.test_model( model=model, sbml_version=sbml_ver, results=True, pytest_args=pytest_args, skip=skip, exclusive=exclusive, experimental=experimental) if collect: if repo is None: manager = ResultManager() manager.store(result, filename=filename) else: LOGGER.info("Checking out deployment branch.") # If the repo HEAD is pointing to the most recent branch then # GitPython's `repo.active_branch` works. Yet, if the repo is in # detached HEAD state, i.e., when a user has checked out a specific # commit as opposed to a branch, this won't work and throw a # `TypeError`, which we are circumventing below. try: previous = repo.active_branch previous_cmt = previous.commit is_branch = True except TypeError: previous_cmt = repo.head.commit is_branch = False repo.git.checkout(deployment) try: manager = SQLResultManager(repository=repo, location=location) except (AttributeError, ArgumentError): manager = RepoResultManager(repository=repo, location=location) LOGGER.info( "Committing result and changing back to working branch.") manager.store(result, commit=previous_cmt.hexsha) repo.git.add(".") check_call( ['git', 'commit', '-m', "chore: add result for {}".format(previous_cmt.hexsha)] ) if is_branch: previous.checkout() else: repo.commit(previous_cmt)