From c8c8060ce051676176d995ce2c90a729c421831d Mon Sep 17 00:00:00 2001 From: Evan Date: Sun, 14 Apr 2019 16:16:17 -0400 Subject: [PATCH] Add test case generator --- LICENSE | 4 +- README.md | 132 ++++++++++++++++- setup.py | 24 ++++ testcase_generator/__init__.py | 8 ++ testcase_generator/generators/__init__.py | 1 + .../generators/graph_generator.py | 136 ++++++++++++++++++ testcase_generator/models.py | 109 ++++++++++++++ testcase_generator/parser.py | 56 ++++++++ 8 files changed, 467 insertions(+), 3 deletions(-) create mode 100644 setup.py create mode 100644 testcase_generator/__init__.py create mode 100644 testcase_generator/generators/__init__.py create mode 100644 testcase_generator/generators/graph_generator.py create mode 100644 testcase_generator/models.py create mode 100644 testcase_generator/parser.py diff --git a/LICENSE b/LICENSE index 0ad25db..49bc716 100644 --- a/LICENSE +++ b/LICENSE @@ -629,8 +629,8 @@ to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - - Copyright (C) + Testcase Generator for online judges. + Copyright (C) 2019 Evan Zhang This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published diff --git a/README.md b/README.md index a1d07d0..66887a6 100644 --- a/README.md +++ b/README.md @@ -1 +1,131 @@ -# testcase-generator \ No newline at end of file +# Testcase Generator + +A testcase generator for easily creating testcases for online judges. + +## Installation +``` +$ pip install testcase-generator +``` + +Alternatively, just clone this repository! + +## Usage +```python +from testcase_generator import Constraint, Case, Batch, Generator, ConstraintParser + +def set_constraints(self): + ## Write main constraints here ## + # Sets the constraint of N to be between 1 and 10^3 inclusive. + self.N = Constraint(1, 10**3) + +def generate_input(self): + ## Write generator here ## + # Generates a value for N + yield self.N.next + + +Case.SET_CONSTRAINTS = set_constraints +Case.SET_INPUT = generate_input + + +# Using the yaml config to create the batches: +config_yaml = """ +- batch: 1 + constraints: {N: 1~10**2} + cases: + - {N: MIN} + - {N: MAX} + - {N: 2~10} + - {N: 10**2-1~} +- batch: 2 + constraints: {} + cases: + - {} + - {N: ~2} +""" + +p = ConstraintParser(data=config_yaml) +p.parse() +batches = p.batches + + +# creating the batches manually +batches = [ + Batch(num=1, cases=[Case() for i in range(4)]), + Batch(num=2, cases=[Case(N=Constraint(1,10)) for i in range(2)]), +] + + +Generator(batches=batches, exe='COMMAND_TO_GENERATE_OUTPUT').start() +``` + +The generator features a `GraphGenerator`, which generates a variety of graph types: +```python +from testcase_generator import Constraint, Case, Batch, Generator, ConstraintParser, GraphGenerator + +""" + | initialize(self, N, graph_type, *args, **kwargs) + | N: number of nodes + | graph_type: + | 1: normal graph + | 2: connected graph + | 3: complete graph + | 4: circle + | 10: line + | 11: normal tree + | 12: tree, all nodes connected to one node + | 13: caterpillar tree + | 14: binary tree + | kwargs: + | M: number of edges, leave blank if it is a tree + | duplicates: allow for duplicate edges between nodes + | self_loops: allow for edges between the same node +""" + +def set_constraints(self): + ## Write main constraints here ## + # Sets the constraint of N to be between 1 and 10^3 inclusive. + # In this case, this is a graph with N nodes. + self.N = Constraint(1, 10**3) + # creates the graph generator + self.ee = GraphGenerator() + # Creates the variable that returns the next edge in the graph. + # The 1s are filler values. + self.E = Constraint(1, 1, self.ee.next_edge) + # Sets the graph type to be some graph type between 10 and 14. + # Please read the initialize method doc for details. + # In this case, the graph type is some form of a tree. + self.graph_type = Constraint(10, 14) + +def generate_input(self): + ## Write generator here ## + n = self.N.next + yield n + self.ee.initialize(n, self.graph_type.next) + for i in range(n-1): + yield self.E.next + + +Case.SET_CONSTRAINTS = set_constraints +Case.SET_INPUT = generate_input + + +# Using the yaml config to create the batches: +config_yaml = """ +- batch: 1 + constraints: {N: 1~10**3-1} + cases: + - {} + - {} + - {} + - {} + - {} + - {} +""" + +p = ConstraintParser(data=config_yaml) +p.parse() +batches = p.batches + +Generator(batches=batches, exe='COMMAND_TO_GENERATE_OUTPUT').start() +``` diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ba49aed --- /dev/null +++ b/setup.py @@ -0,0 +1,24 @@ +from setuptools import find_packages, setup + +with open('README.md') as f: + readme = f.read() + +setup( + name='testcase-generator', + version='0.0.1', + author='Evan Zhang', + description='A testcase generator for creating testcases for online judges.', + long_description=readme, + long_description_content_type="text/markdown", + url='https://github.com/Ninjaclasher/testcase-generator', + packages=find_packages(), + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Environment :: Console', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)', + 'Operating System :: POSIX :: Linux', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3.7', + ], +) diff --git a/testcase_generator/__init__.py b/testcase_generator/__init__.py new file mode 100644 index 0000000..29a8f3a --- /dev/null +++ b/testcase_generator/__init__.py @@ -0,0 +1,8 @@ +""" + Test case generator for online judges. + + Created by Evan Zhang (Ninjaclasher) +""" +from .models import Constraint, Case, Batch, Generator +from .parser import ConstraintParser +from .generators import GraphGenerator diff --git a/testcase_generator/generators/__init__.py b/testcase_generator/generators/__init__.py new file mode 100644 index 0000000..de3457f --- /dev/null +++ b/testcase_generator/generators/__init__.py @@ -0,0 +1 @@ +from .graph_generator import GraphGenerator diff --git a/testcase_generator/generators/graph_generator.py b/testcase_generator/generators/graph_generator.py new file mode 100644 index 0000000..3c85764 --- /dev/null +++ b/testcase_generator/generators/graph_generator.py @@ -0,0 +1,136 @@ +from collections import Counter +from random import Random + + +class GraphGenerator: + def __init__(self): + self.edges = Counter() + self.random = Random() + self.nodes = [] + self.is_initialized = False + + def next_edge(self, a, b): + try: + u, v = self.edges.pop() + except: + return None + if self.random.randint(0, 1) == 0: + return u, v + else: + return v, u + + @property + def _node(self): + return self.random.choice(self.nodes) + + def _generate_nodes(self): + self.nodes = list(range(1, self.N+1)) + self.random.shuffle(self.nodes) + + def _clean(self, a, b): + return (a, b) if a < b else (b, a) + + def _get_node_pair(self): + return self._node, self._node + + def _validate_node_pair(self, a, b): + return (self.self_loops or a != b) and (self.duplicates or self.edges[self._clean(a,b)] == 0) + + def _get_pair(self): + a, b = self._get_node_pair() + while not self._validate_node_pair(a, b): + a, b = self._get_node_pair() + return a, b + + def _add_edge(self, a, b): + self.edges[self._clean(a, b)] += 1 + + def _generate_tree(self): + for i in range(1, self.N): + u = self.random.randint(0, i-1) + self._add_edge(self.nodes[u], self.nodes[i]) + + def _validate(self): + if self.M is None and self.type in (1, 2): + raise ValueError('M must be specified.') + if self.type == 2 and self.M < self.N-1: + raise ValueError('Impossible graph.') + if self.type == 3 and self.N > 10**4: + raise ValueError('Do you want me to TLE?') + + def _generate_edges(self): + N = self.N + + if self.type == 1: + for i in range(self.M): + self._add_edge(*self._get_pair()) + elif self.type == 2: + self._generate_tree() + for i in range(self.M-N+1): + self._add_edge(*self._get_pair()) + elif self.type == 3: + for i in self.nodes: + for j in self.nodes[i + (not self.self_loops):]: + self._add_edge(i, j) + elif self.type == 4: + self.nodes.append(self.nodes[0]) + for i in range(self.N): + self._add_edge(self.nodes[i], self.nodes[i+1]) + elif self.type == 10: + for i in range(self.N-1): + self._add_edge(self.nodes[i], self.nodes[i+1]) + elif self.type == 11: + self._generate_tree() + elif self.type == 12: + special = self._node + for i in self.nodes: + if i != special: + self._add_edge(i, special) + elif self.type == 13: + main_len = self.random.randint(self.N//2, self.N-1) + for i in range(main_len): + self._add_edge(self.nodes[i], self.nodes[i+1]) + for j in range(main_len+1, self.N): + u = self.random.randint(0, j-1) + self._add_edge(self.nodes[u], self.nodes[j]) + elif self.type == 14: + self.nodes = [0] + self.nodes + for i in range(2, self.N+1): + self._add_edge(self.nodes[i], self.nodes[i//2]) + edges = [] + for edge, cnt in self.edges.items(): + edges += [edge] * cnt + self.edges = edges + self.random.shuffle(self.edges) + + def initialize(self, N, graph_type, *args, **kwargs): + """ + N: number of nodes + graph_type: + 1: normal graph + 2: connected graph + 3: complete graph + 4: circle + 10: line + 11: normal tree + 12: tree, all nodes connected to one node + 13: caterpillar tree + 14: binary tree + kwargs: + M: number of edges, leave blank if it is a tree + duplicates: allow for duplicate edges between nodes + self_loops: allow for edges between the same node + """ + if self.is_initialized: + raise ValueError('Cannot intialize twice.') + self.N = N + self.type = int(graph_type) + self.M = kwargs.get('M', None) + self.duplicates = kwargs.get('duplicates', False) + self.self_loops = kwargs.get('self_loops', False) + + self.is_initialized = True + + self._generate_nodes() + self._validate() + self._generate_edges() diff --git a/testcase_generator/models.py b/testcase_generator/models.py new file mode 100644 index 0000000..4b9da70 --- /dev/null +++ b/testcase_generator/models.py @@ -0,0 +1,109 @@ +import os +import random +import types + + +class Constraint: + def __init__(self, MIN, MAX, gen_function=random.randint): + self.MIN = MIN + self.MAX = MAX + self.generate = gen_function + + @property + def bounds(self): + return self.MIN, self.MAX + + @property + def next(self): + return self.generate(*self.bounds) + + def __str__(self): + return '[{}, {}]'.format(*self.bounds) + + def copy(self): + return Constraint(*self.bounds, self.generate) + + +class Case: + SET_CONSTRAINTS = None + SET_INPUT = None + + def __init__(self, *args, **kwargs): + if Case.SET_INPUT is None: + raise ValueError('Case.SET_INPUT is not set to a function.') + if Case.SET_CONSTRAINTS is None: + raise ValueError('Case.SET_CONSTRAINTS is not set to a function.') + + self.set_constraints = types.MethodType(Case.SET_CONSTRAINTS, self) + self.generate_input = types.MethodType(Case.SET_INPUT, self) + + self.set_constraints() + for dic in args: + for name, constraint in dic.items(): + self.set(name, constraint) + for name, constraint in kwargs.items(): + self.set(name, constraint) + + def set(self, var, val): + setattr(self, var, val) + + @property + def dict(self): + return {x[0]: x[1] for x in self.__dict__.items() if type(x[1]) == Constraint} + + def get(self, var): + return self.dict[var] + + def __str__(self): + return '\n'.join('{} = {}'.format(x, y) for x,y in self.dict.items()) + '\n' + + +class Batch: + CASES_DIR = 'cases' + BATCH_DIR = 'batch' + + def __init__(self, num, cases, start=0): + self.batch = num + self.start_case = start + self.cases = cases + + try: + os.mkdir(Batch.CASES_DIR) + except FileExistsError: + pass + try: + os.mkdir(self.location) + except FileExistsError: + pass + + @property + def location(self): + return os.path.join(Batch.CASES_DIR, Batch.BATCH_DIR + str(self.batch)) + + def generate_output(self, exe, filename): + os.system('{0} < {1}.in > {1}.out'.format(exe, filename)) + + def run(self, exe): + for case_num, case in enumerate(self.cases, self.start_case): + filename = os.path.join(self.location, str(case_num)) + with open(filename + '.in', 'w') as out: + for line in case.generate_input(): + try: + iter(line) + except TypeError: + line = str(line) + else: + if type(line) != str: + line = ' '.join(map(str, line)) + out.write(line + '\n') + self.generate_output(exe, filename) + + +class Generator: + def __init__(self, batches, exe): + self.batches = batches + self.exe = exe + + def start(self): + for x in self.batches: + x.run(self.exe) diff --git a/testcase_generator/parser.py b/testcase_generator/parser.py new file mode 100644 index 0000000..130253b --- /dev/null +++ b/testcase_generator/parser.py @@ -0,0 +1,56 @@ +import yaml + +from .models import Case, Batch + + +class ConstraintParser: + def __init__(self, data): + self.batches = [] + self.data = yaml.safe_load(data) + + def parse_case(self, constraints, batch_constraints={}): + constraints_dict = {} + for var, constraint in batch_constraints.items(): + constraints_dict[var] = constraint.copy() + + for var, constraint in constraints.items(): + if var not in constraints_dict.keys(): + constraints_dict[var] = Case().get(var).copy() + + constraint = str(constraint).split('~') + if len(constraint) == 1: + constraint = constraint[0] + if constraint == 'MAX': + constraints_dict[var].MIN = constraints_dict[var].MAX + elif constraint == 'MIN': + constraints_dict[var].MAX = constraints_dict[var].MIN + else: + new_value = eval(constraint) + if not (constraints_dict[var].MIN <= new_value <= constraints_dict[var].MAX): + raise ValueError('{} for constraint {} is not in the ' + 'global or batch constraints'.format(new_value, var)) + constraints_dict[var].MIN = new_value + constraints_dict[var].MAX = new_value + elif len(constraint) == 2: + lower, upper = constraint + if lower.strip(): + constraints_dict[var].MIN = max(constraints_dict[var].MIN, eval(lower)) + if upper.strip(): + constraints_dict[var].MAX = min(constraints_dict[var].MAX, eval(upper)) + if constraints_dict[var].MIN > constraints_dict[var].MAX: + raise ValueError('Lowerbound is larger than upperbound for constraint {}'.format(var)) + else: + raise ValueError + + return constraints_dict + + def parse(self): + for batch in self.data: + batch_constraints = self.parse_case(batch.get('constraints', {})) + + self.batches.append( + Batch( + num=batch['batch'], + cases=[Case(self.parse_case(case, batch_constraints)) for case in batch['cases']], + ) + )