Skip to content

Commit

Permalink
Add test case generator
Browse files Browse the repository at this point in the history
  • Loading branch information
Ninjaclasher committed Apr 14, 2019
1 parent acb203f commit c8c8060
Show file tree
Hide file tree
Showing 8 changed files with 467 additions and 3 deletions.
4 changes: 2 additions & 2 deletions LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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
Expand Down
132 changes: 131 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,131 @@
# testcase-generator
# 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()
```
24 changes: 24 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -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',
],
)
8 changes: 8 additions & 0 deletions testcase_generator/__init__.py
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions testcase_generator/generators/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .graph_generator import GraphGenerator
136 changes: 136 additions & 0 deletions testcase_generator/generators/graph_generator.py
Original file line number Diff line number Diff line change
@@ -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()
Loading

0 comments on commit c8c8060

Please sign in to comment.