From 12a16bf19da1c619bfb307f8c446d1f022c3722e Mon Sep 17 00:00:00 2001 From: Ash Garcia Date: Fri, 22 Jul 2022 17:48:29 -0700 Subject: [PATCH] rewrite --- .gitignore | 3 +- __init__.py | 0 analyze.py | 69 ----------- main.py | 19 ++++ poetry.lock | 32 +++++- pyproject.toml | 1 + smoketest.py | 130 --------------------- smoketest/__init__.py | 1 + smoketest/args.py | 24 ++++ smoketest/runner.py | 177 +++++++++++++++++++++++++++++ storage.py => smoketest/storage.py | 8 +- 11 files changed, 255 insertions(+), 209 deletions(-) delete mode 100644 __init__.py delete mode 100644 analyze.py create mode 100644 main.py delete mode 100644 smoketest.py create mode 100644 smoketest/__init__.py create mode 100644 smoketest/args.py create mode 100644 smoketest/runner.py rename storage.py => smoketest/storage.py (82%) diff --git a/.gitignore b/.gitignore index 1a09d58..1828af4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.bak *.dat *.dir -*.pyc \ No newline at end of file +*.pyc +*.db \ No newline at end of file diff --git a/__init__.py b/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/analyze.py b/analyze.py deleted file mode 100644 index 37444eb..0000000 --- a/analyze.py +++ /dev/null @@ -1,69 +0,0 @@ -import argparse -from collections import Counter, defaultdict -import shelve -from typing import Counter, Dict, List, Optional - -import simfile - -from smoketest import SimfileError - - -def analyze( - simfile_errors: Dict[str, List[SimfileError]], - *, - traceback_substring: str, - verbose: bool, - number: Optional[int] -) -> None: - error_counter = Counter() - error_occurrences = defaultdict(list) - for filename, errors in simfile_errors.items(): - for error in errors: - line = error.traceback.splitlines()[-1] - if not traceback_substring or traceback_substring in line: - error_counter[line] += 1 - error_occurrences[line].append((filename, error.context)) - for line, count in error_counter.most_common(number): - print(f'=== {line} ({count} occurrences) ===') - if verbose: - for filename, context in error_occurrences[line]: - print(f' * "{filename}" (during {repr(context)})') - print() - - -def main(): - argparser = argparse.ArgumentParser() - argparser.add_argument( - '-s', '--shelf', - help='shelf file to use', - default=f'shelf-{simfile.__version__}', - ) - argparser.add_argument( - '-t', '--traceback-substring', - help='filter tracebacks by substring', - ) - argparser.add_argument( - '-n', '--number', - type=int, - default=10, - help='number of most common errors to print', - ) - argparser.add_argument( - '-v', '--verbose', - help='enable verbose output', - default=False, - action='store_true', - ) - args = argparser.parse_args() - - with shelve.open(args.shelf) as simfile_errors: - analyze( - simfile_errors, - traceback_substring=args.traceback_substring, - verbose=args.verbose, - number=args.number, - ) - - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..2ad48b5 --- /dev/null +++ b/main.py @@ -0,0 +1,19 @@ +from smoketest.args import SmoketestArgs +from smoketest.runner import SmoketestRun +from smoketest.storage import DB, MODELS + + +def main(): + args = SmoketestArgs().parse_args() + + DB.connect() + if args.init_db: + DB.create_tables(MODELS) + if args.songs_dir: + SmoketestRun(args).process_songs_dir(args.songs_dir) + elif args.pack_dir: + SmoketestRun(args).process_pack(args.pack_dir) + + +if __name__ == "__main__": + main() diff --git a/poetry.lock b/poetry.lock index 00001bb..a7a1e9b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -93,7 +93,7 @@ python-versions = ">=3.6" name = "mypy-extensions" version = "0.4.3" description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" +category = "main" optional = false python-versions = "*" @@ -161,6 +161,18 @@ category = "dev" optional = false python-versions = ">=3.7" +[[package]] +name = "typed-argument-parser" +version = "1.7.2" +description = "Typed Argument Parser" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +typing_extensions = ">=3.7.4" +typing-inspect = ">=0.7.1" + [[package]] name = "typed-ast" version = "1.5.4" @@ -173,10 +185,22 @@ python-versions = ">=3.6" name = "typing-extensions" version = "4.3.0" description = "Backported and Experimental Type Hints for Python 3.7+" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" +[[package]] +name = "typing-inspect" +version = "0.7.1" +description = "Runtime inspection utilities for typing module." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +mypy-extensions = ">=0.3.0" +typing-extensions = ">=3.7.4" + [[package]] name = "zipp" version = "3.8.1" @@ -192,7 +216,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "4fdaaca625a1242b7a0012eb7261349b6f25459688543e12b9d4c37eed4f8e3a" +content-hash = "86790b53efa0ea295271084a2e0fc85fd169fee4b848238c5ac8d7b71ded07d7" [metadata.files] appdirs = [] @@ -210,6 +234,8 @@ semver = [] simfile = [] six = [] tomli = [] +typed-argument-parser = [] typed-ast = [] typing-extensions = [] +typing-inspect = [] zipp = [] diff --git a/pyproject.toml b/pyproject.toml index 1a184c7..bddc4b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ python = "^3.7" simfile = {version = "^2.1.0b3", allow-prereleases = true} peewee = "^3.15.1" semver = "^2.13.0" +typed-argument-parser = "^1.7.2" [tool.poetry.dev-dependencies] black = "^22.6.0" diff --git a/smoketest.py b/smoketest.py deleted file mode 100644 index fd86242..0000000 --- a/smoketest.py +++ /dev/null @@ -1,130 +0,0 @@ -import argparse -import os -import pprint -import traceback -from typing import Counter, Dict, List, Optional - -from peewee import SqliteDatabase -import simfile -from simfile.notes import NoteData -from simfile.notes.group import group_notes -from simfile.notes.timed import time_notes -from simfile.ssc import SSCChart -from simfile.timing.engine import TimingData -from simfile.timing.displaybpm import displaybpm - -from .storage import * - - -class SimfileError: - context: str - traceback: Optional[str] - - def __init__(self, context: str): - self.context = context - self.traceback = traceback.format_exc() - - def __repr__(self): - return f'{self.context}: {self.traceback}' - -def check_simfile(self, filename: str) -> List[SimfileError]: - errors = [] - try: - sim = simfile.open(filename) - except Exception: - errors.append(SimfileError('parse')) - return errors - - timing_data = None - try: - timing_data = TimingData.from_simfile(sim) - except Exception: - errors.append(SimfileError('timing')) - - try: - displaybpm(sim) - except Exception: - errors.append(SimfileError('displaybpm')) - - for c, chart in enumerate(sim.charts): - try: - note_data = NoteData.from_chart(chart) - except Exception: - errors.append(SimfileError(f'chart {c} note_data')) - try: - for _ in group_notes(note_data, join_heads_to_tails=True): - pass - except Exception: - errors.append(SimfileError(f'chart {c} group_notes')) - - if isinstance(chart, SSCChart): - try: - displaybpm(sim, chart) - except Exception: - errors.append(SimfileError(f'chart {c} displaybpm')) - - # skip the remaining chart tests if timing data parsing failed - if timing_data is None: - continue - - if isinstance(chart, SSCChart): - try: - timing_data = TimingData.from_simfile(sim, chart) - except Exception: - errors.append(SimfileError(f'chart {c} timing')) - - try: - for _ in time_notes(note_data, timing_data): - pass - except Exception: - errors.append(SimfileError(f'chart {c} timed_notes')) - - return errors - - -def scan_songs_dir(self, path: str): - stats = Counter() - for entry in os.scandir(path): - if simfile_errors.get(entry.path) == []: - stats['skipped'] += 1 - continue - if entry.is_dir(): - stats.update(scan_pack(entry.path)) - elif any(entry.name.endswith(ext) for ext in ('.sm', '.ssc')): - errors = check_simfile(entry.path) - stats['checked'] += 1 - simfile_errors[entry.path] = errors - if errors: - stats['error'] += 1 - pprint.pprint({'path': entry.path, 'errors': errors}) - else: - stats['success'] += 1 - - return stats - - - -def dir_path(string): - if os.path.isdir(string): - return string - else: - raise ValueError('dir_path: not a directory') - - -def main(): - argparser = argparse.ArgumentParser() - argparser.add_argument( - 'd', - '--songs-dir', - help='directory of packs to scan', - type=dir_path, - ) - args = argparser.parse_args() - - scan_songs_dir(args.songs_dir) - for k, v in stats.items(): - print(k, v) - - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/smoketest/__init__.py b/smoketest/__init__.py new file mode 100644 index 0000000..3dc1f76 --- /dev/null +++ b/smoketest/__init__.py @@ -0,0 +1 @@ +__version__ = "0.1.0" diff --git a/smoketest/args.py b/smoketest/args.py new file mode 100644 index 0000000..b6ef0b4 --- /dev/null +++ b/smoketest/args.py @@ -0,0 +1,24 @@ +import os +from typing import Optional + +from tap import Tap + + +__all__ = ["SmoketestArgs"] + + +class SmoketestArgs(Tap): + songs_dir: Optional[str] = None + """directory of packs to scan""" + + pack_dir: Optional[str] = None + """single pack directory to scan""" + + new_only: bool = False + """only scan newly discovered simfiles""" + + error_only: bool = False + """only scan simfiles that have previously errored""" + + init_db: bool = False + """initialize the database with tables""" diff --git a/smoketest/runner.py b/smoketest/runner.py new file mode 100644 index 0000000..97cc20a --- /dev/null +++ b/smoketest/runner.py @@ -0,0 +1,177 @@ +from contextlib import contextmanager +import dataclasses +from datetime import datetime +import os +import sys +import traceback +from typing import Iterator, Optional + +from . import __version__ +import msdparser +import simfile +from simfile.dir import SimfilePack +from simfile.notes import NoteData +from simfile.notes.group import group_notes, SameBeatNotes +from simfile.notes.timed import time_notes +from simfile.ssc import SSCChart +from simfile.timing.engine import TimingData +from simfile.timing.displaybpm import displaybpm +from simfile.types import Simfile, Chart + +from .args import SmoketestArgs +from .storage import * + + +@dataclasses.dataclass(frozen=True) +class SimfileContext: + run: Run + path: str + kind: Optional[str] = None + simfile_title: Optional[str] = None + chart_stepstype: Optional[str] = None + chart_meter: Optional[str] = None + chart_index: Optional[int] = None + + def report_error(self, action: str): + print(f"{self.path} ({self.kind}): {traceback.format_exc()}") + simfile_object, _ = SimfileObject.get_or_create( + path=self.path, + kind=self.kind, + simfile_title=self.simfile_title, + chart_stepstype=self.chart_stepstype, + chart_meter=self.chart_meter, + chart_index=self.chart_index, + ) + SimfileError.create( + action=action, + traceback=traceback.format_exc(), + simfile_object=simfile_object, + run=self.run, + ) + + @contextmanager + def _update(self, **kwargs) -> Iterator["SimfileContext"]: + yield dataclasses.replace(self, **kwargs) + + @contextmanager + def object(self, kind, **kwargs) -> Iterator["SimfileContext"]: + with self._update(kind=kind, **kwargs) as context: + SimfileObject.get_or_create( + kind=kind, + path=context.path, + simfile_title=context.simfile_title, + chart_stepstype=context.chart_stepstype, + chart_meter=context.chart_meter, + chart_index=context.chart_index, + ) + yield context + + @contextmanager + def perform(self, action: str, **kwargs) -> Iterator["SimfileContext"]: + with self._update(**kwargs) as context: + try: + yield context + except Exception: + context.report_error(action) + + +class SmoketestRun: + args: SmoketestArgs + run: Run + + def __init__(self, args: SmoketestArgs): + self.args = args + self.run = Run.create( + created=datetime.now(), + simfile_version=simfile.__version__, + msdparser_version=msdparser.__version__, + smoketest_version=__version__, + ) + + def process_songs_dir(self, songs_dir: str): + for entry in os.scandir(songs_dir): + if entry.is_dir(): + self.process_pack(entry.path) + + def process_pack(self, pack_dir: str): + root_context = SimfileContext(run=self.run, path=pack_dir) + with root_context.object("SimfilePack", path=pack_dir) as context: + print(f"Processing pack {pack_dir}") + pack = SimfilePack(pack_dir) + + with context.perform("simfile_dirs") as context: + for simfile_dir in pack.simfile_dirs(): + if simfile_dir.sm_path: + self._open_simfile(context, simfile_dir.sm_path) + if simfile_dir.ssc_path: + self._open_simfile(context, simfile_dir.ssc_path) + + def _open_simfile(self, context: SimfileContext, simfile_path: str): + if self.args.new_only: + if SimfileObject.get_or_none(SimfileObject.path == simfile_path): + return + elif self.args.error_only: + if not ( + SimfileError.select() + .join(SimfileObject) + .where(SimfileObject.path == simfile_path) # type: ignore + .get_or_none() + ): + return + + with context.perform("simfile.open", path=simfile_path) as context: + sim = simfile.open(simfile_path) + + with context.object( + kind=sim.__class__.__name__, + simfile_title=sim.title, + ) as context: + self._test_simfile(context, sim) + + def _test_simfile(self, context: SimfileContext, sim: Simfile): + with context.perform("TimingData") as context: + TimingData(sim) + + with context.perform("displaybpm") as context: + displaybpm(sim) + + with context.perform("charts") as context: + for c, chart_ in enumerate(sim.charts): + chart: Chart = chart_ # typing workaround + with context.object( + chart.__class__.__name__, + chart_stepstype=chart.stepstype, + chart_meter=chart.meter, + chart_index=c, + ) as context: + self._test_chart(context, sim, chart) + + def _test_chart(self, context: SimfileContext, sim: Simfile, chart: Chart): + td = None + if isinstance(chart, SSCChart): + with context.perform("TimingData+ssc") as context: + td = TimingData(sim, chart) + with context.perform("displaybpm+ssc") as context: + displaybpm(sim) + else: + # We've already performed this action. Don't double-report any error + try: + td = TimingData(sim) + except Exception: + pass + + with context.perform("NoteData") as context: + nd = NoteData(chart) + + with context.perform("group_notes") as context: + for _ in group_notes( + iter(nd), + join_heads_to_tails=True, + same_beat_notes=SameBeatNotes.JOIN_ALL, + ): + pass + + if td: + with context.perform("time_notes") as context: + for _ in time_notes(nd, td): + pass diff --git a/storage.py b/smoketest/storage.py similarity index 82% rename from storage.py rename to smoketest/storage.py index cdf46e3..9281b0d 100644 --- a/storage.py +++ b/smoketest/storage.py @@ -9,10 +9,6 @@ class Run(Model): simfile_version = CharField() msdparser_version = CharField() smoketest_version = CharField() - success = IntegerField() - error = IntegerField() - checked = IntegerField() - skipped = IntegerField() class Meta: database = DB @@ -31,7 +27,7 @@ class SimfileObject(Model): class SimfileError(Model): - context = CharField() + action = CharField() traceback = CharField(max_length=10000) simfile_object = ForeignKeyField(model=SimfileObject) run = ForeignKeyField(model=Run) @@ -40,4 +36,4 @@ class SimfileError(Model): database = DB -DB.connect() +MODELS = [Run, SimfileObject, SimfileError]