tcframe Documentation¶
Welcome to the official documentation of tcframe, a competitive programming test cases generator framework! This documentation is organized into several sections:
Introduction¶
General information of tcframe project. Contains some code examples. Start here to grasp general understanding of this framework.
Getting Started¶
After getting the high-level concept of the framework, why not actually start writing a test cases generator? This section will guide you to install and write your very first generator.
Topic Guides¶
Now that you got your hand wet with your first generator, master each of tcframe‘s key concepts in more details here.
API Reference¶
Finally, this is an API documentation of all tcframe features that you will be looking up often when writing your generator.
Introduction¶
tcframe is a C++ framework for generating test cases of competitive programming problems. This framework helps problem writers prepare test cases in a structured manner and ensures that the generated test cases are valid according to the specified constraints.
At a very high level, with some details omitted, it works as follows:
You specify input/output variables.
int A, B; int sum;
You specify input/output formats.
void InputFormat() { LINE(A, B); // A line containing space-separated A and B } void OutputFormat() { LINE(sum); }
You specify the constraints.
void Constraints() { CONS(1 <= A && A <= 1000); CONS(1 <= B && B <= 1000); }
You specify the sample test cases.
void SampleTestCases() { SAMPLE_CASE({ "2 8" }); SAMPLE_CASE({ "42 100" }); }
You specify the official test cases. Random number generator is available.
void TestCases() { CASE(A = 1, B = 1); CASE(A = 77, B = 99); CASE(A = rnd.nextInt(1, 1000), B = rnd.nextInt(1, 1000)); }
You write the official solution to this problem, using any programming language you wish. Oh, haven’t we told you what the actual problem we have been talking about is? Yes, it is the infamous A+B problem!
#include <iostream> using namespace std; int main() { int A, B; cin >> A >> B; cout << (A + B) << endl; }
You run the generator. Actual test cases (.in and .out files) will be generated. Profit!
If you ever specified an invalid test case, such as
CASE(A = 0, B = 1)
, you will get a nice error message:aplusb_4: FAILED Description: A = 0, B = 1 Reasons: * Does not satisfy constraints, on: - 1 <= A && A <= 1000
Features¶
As of the current version, tcframe supports:
- Standard batch problems; i.e., problems which requires the solution to read from stdin and print to stdout.
- Constraints specified in IOI-style subtasks.
- Multiple test cases per file.
- Simulating submission against the generated test cases.
- Specifying time and memory limits.
- Basic random number generation helper.
Requirements¶
tcframe requires:
- Linux/OS X. Windows is currently not supported yet
- GCC ≥ 4.7. tcframe relies heavily on C++11 features
Frequently Asked Questions¶
Why do we even need to write a generator for test cases, in the first place?
- That’s primarily because writing test cases manually is error-prone and time-consuming.
- To enable distributing the test cases as a single, small generator file. No need to send 20 MB testcases.zip over email anymore.
- During problem development, constraints often change. Using a generator, we can easily fix the constraint and just run the generator again.
OK. But why do we need a framework for that?
- The main problem is that not all people know how to write a good test cases generator.
- To avoid writing repetitive and boring tasks. For example: creating test case files with correct suffixes (foo_1.in, foo_1.out), running official solution against the test case input files, etc.
- To make all problems in a contest have test cases generator with consistent format.
Credits¶
tcframe is being heavily developed by Ashar Fuadi. It is based on a paper submitted to IOI conference in 2015: Introducing tcframe: A Simple and Robust Test Cases Generation Framework, written by the same author.
tcframe was mainly inspired from testlib, written by Mike Mirzayanov et al.
Getting Started¶
Let’s write our first generator! We are going to write a test cases generator for the following infamous “A+B” problem:
4 6
10
Note
This starter guide will just demonstrate the basic features of tcframe. For more advanced features, please consult the Topic Guides afterwards.
Installation¶
Firstly, we must get tcframe on our system. Fortunately, tcframe consists of just C++ header files: we don’t actually need to “install” anything; we just have to #include
the header files in our program and we are ready to go.
Download the latest tcframe here: https://github.com/fushar/tcframe/releases/download/v0.7.0/tcframe_0.7.0.zip.
Extract the zip file somewhere on your system. For example, extract it to ~/tcframe
.
Preparing working directory¶
Create a directory called aplusb
somewhere on your system; for example in your home (~/aplusb
). This will be our working directory for A+B problem. Store all the files you will be creating on this guide (solution and runner) in this directory.
Writing solution program¶
In order to be able to generate the test case output files, a reference solution must be available. For the purpose of this guide, call the solution file solution.cpp
. Example solution:
#include <iostream>
using namespace std;
int main() {
int A, B;
cin >> A >> B;
cout << (A + B) << endl;
}
Then, compile it to solution
executable, for example:
g++ -o solution solution.cpp
Writing runner program¶
Now, we are ready to write the generator. In tcframe terms, the generator program is actually called a runner program, because it contains not only the generator but also the problem specification itself. Don’t worry – this will be clearer soon.
Create a C++ source file called runner.cpp
. Copy-paste the following code to the file:
#include <tcframe/runner.hpp>
using namespace tcframe;
class Problem : public BaseProblem {
protected:
int A, B;
int sum;
void Config() {
setSlug("aplusb");
setTimeLimit(1);
setMemoryLimit(64);
}
void InputFormat() {
LINE(A, B);
}
void OutputFormat() {
LINE(sum);
}
void Constraints() {
CONS(1 <= A && A <= 1000);
CONS(1 <= B && B <= 1000);
}
};
class Generator : public BaseGenerator<Problem> {
protected:
void Config() {
setTestCasesDir("tc");
setSolutionCommand("./solution");
}
void SampleTestCases() {
SAMPLE_CASE({
"4 6"
});
}
void TestCases() {
CASE(A = 1, B = 1);
CASE(A = 1000, B = 1000);
CASE(A = 42, B = 100);
CASE(A = rnd.nextInt(1, 1000), B = rnd.nextInt(1, 1000));
}
};
int main(int argc, char* argv[]) {
Runner<Problem> runner(argc, argv);
runner.setGenerator(new Generator());
return runner.run();
}
We will explain this runner program later – keep going!
Next, we will compile this runner program against tcframe headers. To do that, we need to add -I [/path/to/tcframe]/include
and -std=c++11
as compile options to g++. For example:
g++ -I ~/tcframe/include -std=c++11 -o runner runner.cpp
Make sure that it compiles before continuing this getting started guide!
Finally, run the runner:
./runner
If everything is OK, you should get the following output:
Generating test cases...
[ SAMPLE TEST CASES ]
aplusb_sample_1: OK
[ OFFICIAL TEST CASES ]
aplusb_1: OK
aplusb_2: OK
aplusb_3: OK
aplusb_4: OK
Congratulations, you have just written a runner program using tcframe framework! Check out your aplusb/tc
directory – it will contain the generated test case files.
Inspecting runner program¶
We will now examine each component of the runner program in more details.
tcframe header¶
#include <tcframe/runner.hpp>
using namespace tcframe;
The tcframe/runner.hpp
is the main tcframe‘s header file for runner programs. Each component of tcframe resides in the tcframe
namespace, just like the STL functions that reside in the std
namespace. By importing the namespace, we don’t have to explicitly prefix each class/object we want to use with tcframe::
.
Problem specification class¶
class Problem : public BaseProblem {
protected:
...
};
A problem specification class is where we define the I/O format and constraints of our problem. This class must inherit tcframe::BaseProblem
. We just chose Problem
as the class name for simplicity.
All required members of this class must go in the protected section.
Problem configuration¶
void Config() {
setSlug("aplusb");
setTimeLimit(1);
setMemoryLimit(64);
}
What’s going on here? We just specified several properties of our problem, that can be done in the Config()
method. setTimeLimit()
and setMemoryLimit()
should be self-explanatory. setSlug()
sets, well, the slug. A slug is a simple name/codename/identifier for the problem. The produced test cases will have the slug as the prefix of each test case file. We picked aplusb
for this particular problem.
Input/output variables and formats¶
int A, B;
int sum;
void InputFormat() {
LINE(A, B);
}
void OutputFormat() {
LINE(sum);
}
Next, we defined the input and output variables and formats. The input consists of two values: A and B. The output consists of one value; let’s call it sum. We must declare a variable for each of those values, and then tell tcframe how to format them in the input/output files.
Here, we declared two integers A
and B
as input variables, and an integer sum
as an output variable. InputFormat()
and OutputFormat()
methods specify the input/output formats in terms of the input/output variables. The LINE()
macro here specifies a line consisting of space-separated values of the given arguments.
Constraints¶
void Constraints() {
CONS(1 <= A && A <= 1000);
CONS(1 <= B && B <= 1000);
}
The last part of a problem specification is constraints specification.
A constraint must depend on input variables only. Each constraint can be specified as a boolean predicate inside the CONS()
macro.
Here, we have two constraints, which are just direct translations of what we have in the problem statement.
We now have a formal specification of our A+B problem. The next part is writing a generator that produces test cases which conform to that problem specification.
Generator specification class¶
class Generator : public BaseGenerator<Problem> {
protected:
...
};
A generator specification is a class that inherits tcframe::BaseGenerator<T>
, where T
is the problem specification class. As usual, the name Generator
is just for simplicity – it can be anything else.
This is where we actually write the test case definitions.
Generator configuration¶
void Config() {
setTestCasesDir("tc");
setSolutionCommand("./solution");
}
Similar to the problem specification, we can set some properties of the generator with Config()
method.
Here, we tell tcframe to put all generated test case files in tc/
directory (relative to the current directory), and to use ./solution
command to generate the output of each input file.
Test case definitions¶
void SampleTestCases() {
SAMPLE_CASE({
"4 6"
});
}
void TestCases() {
CASE(A = 1, B = 1);
CASE(A = 1000, B = 1000);
CASE(A = 42, B = 100);
CASE(A = rnd.nextInt(1, 1000), B = rnd.nextInt(1, 1000));
}
Here, we finally defined the test cases (yeay!). For the purpose of this guide, we defined four test cases: 3 hand-made and 1 randomized. We also defined one sample test case that match with the one in the actual problem statement.
In tcframe, sample test cases, if any, are defined in the SampleTestCases()
method. Each sample test case is defined as line-by-line verbatim strings in the SAMPLE_CASE()
macro. Sample test cases must conform to the input format, or tcframe will complain.
Test cases are defined in the TestCases()
method. Each test case is defined by listing input variable assignments the CASE()
macro, separated by commas. Here, we just defined a min case, max case, random hand-made case, and a randomized case. The last one is achieved using tcframe::rnd
, a simple random number generator provided by tcframe.
Note
Yes, you can access the input variables directly inside the generator, even though they are declared in the problem specification class!
That’s it for generator specification class. Problem and generator specification classes will be then managed by our main()
function.
Main function¶
int main(int argc, char* argv[]) {
Runner<Problem> runner(argc, argv);
runner.setGenerator(new Generator());
return runner.run();
}
The specification classes are ultimately instantiated here. We constructed runner object of our problem, set the generator, and then ran it.
Note
In most cases, you would want to just copy-paste this main()
function to your runner programs – you don’t have to modify it at all.
We’ve covered each component of a our runner program in more details. Next, let’s play around with our runner program.
Trying out invalid test cases¶
What happens when we specify invalid test cases? Let’s just try. Add this test case to our generator:
CASE(A = 0, B = 1);
and this sample test case:
SAMPLE_CASE({
"1",
"2"
});
Recompile and rerun the runner program. You should now get the following output instead:
Generating test cases...
[ SAMPLE TEST CASES ]
aplusb_sample_1: OK
aplusb_sample_2: FAILED
Reasons:
* Expected: <space> after variable `A`
[ OFFICIAL TEST CASES ]
aplusb_1: OK
aplusb_2: OK
aplusb_3: OK
aplusb_4: OK
aplusb_5: FAILED
Description: A = 0, B = 1
Reasons:
* Does not satisfy constraints, on:
- 1 <= A && A <= 1000
Sweet! If we ever have invalid test cases, tcframe will tell us in human-readable message.
Remove the invalid test cases and move on to the next section.
Simulating submission¶
When preparing a problem, it’s ideal if we have at least another solution as an alternative/secondary solution. tcframe lets you “submit” another solution using the main solution as the reference.
First, fix our runner and re-run it to get back the correct test cases (important!). Then, write an alternate solution that deliberately behaves incorrectly on some test cases. Write the following as solution_alt.cpp
:
#include <iostream>
using namespace std;
int main() {
int A, B;
cin >> A >> B;
if (A == 1) {
cout << 3 << endl;
} else if (A == 1000) {
while (true);
} else if (A == 42) {
return 1 / 0;
} else {
cout << (A + B) << endl;
}
}
Compile the solution into solution_alt
executable, and then run the following command:
./runner submit --solution-command=./solution_alt
The above command tells tcframe to run the specified alternate solution command against the output files previously produced by the main solution.
You should get the following output:
Submitting...
[ SAMPLE TEST CASES ]
aplusb_sample_1: Accepted
[ OFFICIAL TEST CASES ]
aplusb_1: Wrong Answer
* Diff:
(expected) [line 01] 2
(received) [line 01] 3
aplusb_2: Time Limit Exceeded
aplusb_3: Runtime Error
* Execution of submission failed:
- Floating point exception: 8
aplusb_4: Accepted
[ RESULT ]
Time Limit Exceeded
We get a detailed verdict of each test case. Nice, isn’t it? The final result here is Time Limit Exceeded, which is the “worst” verdict among all test case verdicts.
We’ve covered the basics of tcframe. At this point, continue reading Topic Guides for more in-depth explanation of tcframe features.
Topic Guides¶
In this guide, we will learn each aspect of tcframe more thoroughly. Each section may have notes at the end , explaining the current limitations and future plans of the corresponding aspect.
Runner program¶
The core activity when preparing test cases with tcframe is writing runner programs. A runner program, along with a reference solution program, completely define the test cases of a single problem. A runner program is compilable into a single executable, which generates the test cases when run.
To write a runner program, create a C++ source file, and include the following header:
#include <tcframe/runner.hpp>
Every component of tcframe falls under tcframe
namespace, so you might want to import it for convenience:
using namespace tcframe;
Components of a runner program¶
Each runner program consists of the following components.
- Problem specification
- Where you specify the problem’s time/memory limits, I/O variables, I/O formats, constraints, etc. It is a very formal specification of what are stated in the problem description.
- Generator specification
- Where you define the sample and official test cases, which solution to use, where to store the generated test cases, etc. It is tied to a particular problem specification: each test case must conform to the problem constraints, etc.
Writing main() function¶
To be able to be compiled into an executable, a runner program must have a main()
function. Currently, there is only one way to write the main()
function, as follows.
int main(int argc, char* argv[]) {
Runner<Problem> runner(argc, argv);
runner.setGenerator(new Generator());
return runner.run();
}
In most cases, you would want to just copy and paste the above snippet as your main()
function.
Compiling runner program¶
You must have g++
at least version 4.7 to compile a runner program. It must be compiled with the following additional options:
-I <tcframe-location>/include
, wheretcframe-location
is where you downloaded and extracted tcframe.-std=c++11
, since tcframe relies on C++11 features.
For example:
g++ -I ~/tcframe/include -std=c++11 -o runner runner.cpp
Running runner program¶
A runner program is an ordinary executable program. It can be run by executing it:
./runner
The above execution will produce the actual test case files.
Notes¶
- A runner consists of only problem and generator specifications. We are planning to support other components, for example, checker and inline solution specifications.
- A problem specification and the corresponding generator specification must be in the same file. We might support separating the specifications into two files.
Problem specification¶
A problem specification is a formal representation of a problem statement/description. It is the first step and the source of truth of the entire preparation of a problem. Once a problem specification is finalized, the corresponding generator specification and a reference solution can then be written.
In tcframe, a problem specification is implemented as a class that inherits tcframe::BaseProblem
. The class name can be arbitrary, but usually there is no reason to name the class other than Problem
.
class Problem : public BaseProblem {
protected:
...
};
All members of this class must go in the protected section, except for private helper methods.
Configuring problem¶
Several properties of the problem can be configured by overriding Config()
method. In the method, we can set a property by calling the appropriate method as follows.
- Slug
- By calling
setSlug(<slug>)
. A slug is a simple name, codename, or identifier for the problem. For example:aplusb
,wf-2016-a
,tree-paint
, etc. The resulting test case files will be prefixed by the slug, for example:aplusb_1.in
. - Time limit
- By calling
setTimeLimit(<timeLimit>)
. Time limit is specified in seconds. Time limit will be enforced when simulating submissoin. - Memory limit
- By calling
setTimeLimit(<memoryLimit>)
. Memory limit is specified in megabytes. Similarly, it will be enforced when simulating submission.
Here is an example of a valid problem configuration:
void Config() {
setSlug("rabbit-jump");
setTimeLimit(2);
setMemoryLimit(256);
}
I/O variables¶
Input variables are C++ variables whose values compose test cases input files. They are are direct translations from variables mentioned in the problem statement’s input format section into C++ variables. For example, given the following description:
The first line contains two integers N and K. The second line contains N space-separated integers A[1], A[2], ..., A[N].
we will have three input variables: int N
, int K
, and vector<int> A
.
Input variables are the basic units of computation in tcframe. For example, they are manipulated in three different places:
- Input format definition in problem specification. It determines the layout of test case input files.
- Constraint definitions in problem specification. They determine whether the values of input variables are valid.
- Test case definitions in generator specification. Each of them determines the content of a single test case input file.
Output variables are defined in a similar way, the only difference being that they are just manipulated in output format definition. Also, usually they consist of just a single variable without name in the problem statement’s output format. We can just name them result
or something similar.
I/O variables are defined as protected member variables in the problem specification class. For example:
class Problem : public BaseProblem {
protected:
int N, K;
vector<int> A;
string result;
// the rest of problem specification components follow
};
Supported I/O variable types¶
tcframe supports three types of I/O variables as follows.
- Scalar
- Variables of built-in integral types (
int
,long long
,char
, etc.), built-in floating-point types (float
,double
), andstd::string
. - Vector
std::vector<T>
, whereT
is a scalar type as defined above.- Matrix
std::vector<std::vector<T>>
, whereT
is a scalar type as defined above.
Other types are not supported as I/O variables. tcframe prefers STL types whenever possible. For example, char*
is not supported as strings. Also, regular arrays (T[]
) and 2D arrays (T[][]
) are not supported.
I/O formats¶
Input format specifies two things at the same time:
- How the input variable values should be printed to official test case input files.
- How the sample test case input files should be parsed into input variables, as they are specified as literal strings. If some sample sample test cases can’t be parsed, it means that you have written invalid sample test cases, and the whole generation will fail.
Output format specifies how the output files should be parsed into output variables. If the output files can’t be parsed, it means that you have written invalid solution program, and the whole generation will fail.
Each of the I/O formats is defined as a list of I/O segments in InputFormat()
and OutputFormat()
methods of the problem specification class, respectively, as follows.
void InputFormat() {
// list of input segments
}
void OutputFormat() {
// list of output segments
}
Each I/O segment is specified by a function-like C++ macro. The I/O segments are specified one-by-one, each as a statement that calls the macro. For example:
void InputFormat() {
LINE(N, M);
EMPTY_LINE();
LINE(A % SIZE(N));
}
Supported I/O segments¶
The list of supported segments is given below. In all segments, it must be noted that:
- The terms scalar, vector, and matrix below refer to the I/O variable types described in the previous chapter.
- Vectors and matrices will have 0-based indexing. This is strict and there is no way to enforce 1-based indexing.
- Empty line
- Specified by the macro
EMPTY_LINE()
. Represents a single, empty line. - Singe line
Specified by the macro
LINE(...)
. Represents space-separated values in a single line.The macro accepts one or more I/O variables as arguments. The value of the variables will be space-separated. For example: ”... the first line consists of two space-separated integers N and M” can be specified by
LINE(N, M)
.Besides scalars, the arguments can be vectors, too. You need to specify the number of elements using
% SIZE(<size>)
syntax after each vector. For example: ”... the next line consists of N space-separated integers A[0], A[1], ... A[N-1]” can be specified byLINE(A % SIZE(N))
. The size can be a constant as well; for example,LINE(B % SIZE(3))
.You can freely combine scalars and vectors in
LINE()
; for example,LINE(N, A % SIZE(N), C % SIZE(2))
would represent a line consisting ofN A[0] A[1] ... A[N-1] C[0] C[1]
.Lastly, you can specify a vector without fixed size, as long as it is the last argument. For example: ”... the input consists an integer X followed by space-separated integers” can be specified by
LINE(X, A)
(assumingA
is a vector).- Multiple lines
Specified by the macro
LINES(...) % SIZE(<size>)
. Represents multiple lines, each containing an element of a vector given as the argument.For example: ”... each of the next N lines contains an integer X[i]” can be specified by
LINES(X) % SIZE(N)
.There could be more than one vector as well. For example: ”... the next N lines each contains space-separated integers X[i] and Y[i]” can be specified by
LINES(X, Y) % SIZE(N)
. If there are multiple vectors, they all must have the same number of elements.You also could specify jagged vector as the last argument. This is useful for input format like, ”... then M lines follow. Each line begins with a string op. If op is
UPDATE
, then it is followed by two integers x y. If it isQUERY
, then it is followed by a single integer z”. This can be specified byLINES(op, data)
whereop
is avector<string>
anddata
is avector<vector<int>>
. Then,data[i]
represents a vector of integers in the i-th line, which can consist of one of two elements.- Grid
Specified by the macro
GRID(...) % SIZE(<row>, <column>)
. Represents a grid containing elements of a matrix given as the argument, laid out in a grid.For example: ”... each fo the next R lines contain C integers G[i][j]” can be specified by
GRID(G) % SIZE(R, C)
.If the matrix is of type
char
, the elements in each row is not space-separated, otherwise they are space-separated.
For more details, consult the API reference for I/O segments.
Notes¶
Unfortunately, the following are not supported (yet):
- Constants in I/O segments
For example: ”... the first line will always consist of the string
BEGIN
.” Everything must be wrapped in variables.As a workaround, just create an input variable and initialize it to
BEGIN
.- Complex conditional I/O format that can’t be handled by jagged vectors
There is NO known general workaround yet. We’re still working on designing how to handle complex format.
However, there are workarounds for simple cases, for example:
“Output the required sum, or the string
IMPOSSIBLE
if there is no solution.”In this case, you can just use a string as the output variable. The downside is that it is not type-safe; for example, the generation won’t fail if the reference solution mistakenly output an invalid string such as
123abc
.
However, a last resort for a workaround does exist for output format. If you have complex output format, you can just omit the method OutputFormat()
altogether and your solution’s output won’t be checked at all for validity.
Constraints¶
Constraints are conditions that must be satisfied by the input variables. The conditions will be verified on each test case. If any of the constraints is not satisfied, then the generation is considered to fail on that test case. There will be a nice error message explaining which constraints are not satisfied.
The truth value of a constraint must depend only on the values of the input variables in order for it to be a semantically correct constraint.
Constraints can be specified in the Constraints()
method of the problem specification class.
void Constraints() {
// list of constraint definitions
}
Constraint definitions¶
Constraints are specified as a list of constraint definitions. Each constraint definition is a boolean predicate of some property of the input variables. The predicate is passed as an argument to the CONS()
macro. The macros are then called one-by-one as method calls:
void Constraints() {
CONS(<predicate 1>);
CONS(<predicate 2>);
...
}
The constraint definition can be pulled directly from the constraints section in the actual problem description. For example: “1 ≤ A ≤ 1,000” can be specified by CONS(1 <= A && A <= 100)
. “1 ≤ A, B ≤ 1,000” translates into two specifications: CONS(1 <= A && A <= 1000)
and CONS(1 <= B && B <= 1000)
. “1 ≤ A ≤ B ≤ 1,000” translates into CONS(1 <= A && A <= B && B <= 1000)
.
How about more complex predicates, such as “1 ≤ A[i] ≤ 1,000”? You can write a private method for this purpose. For example, it can be translated into CONS(eachElementBetween(A, 1, 1000))
and a private method as follows:
bool eachElementBetween(const vector<int>& X, int lo, int hi) {
for (int x : X) {
if (x < lo || x > hi) {
return false;
}
}
return true;
}
This also applies to even more complex predicates, such as “It is guaranteed that the given graph is a tree”. This can be translated to CONS(graphIsTree())
and define the appropriate private boolean method.
Notes¶
It is tedious to write eachElementBetween()
predicate over and over again as it is very common in problems. We are working on providing such helper methods as core part of tcframe in the next versions.
Subtasks¶
The concept of subtasks was first formally introduced in IOI 2010. The idea is basically to split a problem into two or more smaller problems with gradually increasing difficulty, and assign them different points.
For example:
In this type type of problem, you won’t be writing a Constraints()
method. Instead, you will be writing SubtaskX()
methods, where X
is the subtask number.
Note
As of this version, you can define up to 10 subtasks: Subtask1() .. Subtask10().
Subtask definitions¶
A subtask is basically just a set of constraints. It can be specified by the method SubtaskX()
, where X
is the subtask number. Inside the method, the constraint specifications are given, similar to what you usually do in the Constraints()
method.
Thus, the above example can be implemented as:
void Subtask1() {
CONS(N == 1);
CONS(1 <= K && K <= 100);
}
void Subtask2() {
CONS(1 <= N && N <= 100);
CONS(K == 1);
}
void Subtask3() {
CONS(1 <= N && N <= 100);
CONS(1 <= K && K <= 100);
}
Test groups¶
If your problem has subtasks, you will be writing test groups, not test cases. Refer to this chapter on how to do that.
Notes¶
Currently, tcframe does not provide a way to specify subtask points. It is up to the grader to implement the scoring.
Generator specification¶
A generator specification defines the actual test cases for a particular problem specification. For problems with subtasks, it also defines how the test cases are distributed to the subtasks.
In tcframe, a generator specification is implemented as class that inherits tcframe::BaseGenerator<TProblem>
. Here, TProblem
is the problem specification class, which is usually Problem
. The class name can be arbitrary, but usually there is no reason to name the class other than Generator
.
class Generator : public BaseGenerator<Problem> {
protected:
...
};
All members of this class must go in the protected section, except for private helper methods.
Configuring generator¶
Several properties of the problem can be configured by overriding Config()
method. In the method, we can set a property by calling the appropriate method as follows.
- Test cases directory
- By calling
setTestCasesDir(<directory>)
. This directory will contain the generated test cases. The directory is specified as a relative path to the runner. The default value istc
. - Solution command
- By calling
setSolutionCommand(<command>)
. The command for executing the reference solution. The default value is./solution
.
Here is an example of a valid problem configuration:
void Config() {
setTestCasesDir("generated-tc");
setSolutionCommand("java Solution");
}
For simplicity, usually you don’t have to specify this method at all and just accept/use the default values.
Test cases¶
A test case is a state of input variables. If the values of the input variables conform to each of the specified constraints, then the test case is considered valid, otherwise it is invalid.
Test cases are defined in the TestCases()
method of the problem specification class.
void TestCases() {
// list of test case definitions
}
A sample test case is similar to a test case, except that it is not secret and appears in the problem statement. Sample test cases are defined in the SampleTestCases()
method of the problem specification class.
void SampleTestCases() {
// list of sample test case definitions
}
Test case definition¶
Test cases are specified as a list of test case definitions. Each test case definition is a comma-separated list of input variable assignments. The list is passed as the argument to the CASE()
macro. The macros are then called one-by-one as method calls:
void TestCases() {
CASE(<assignments list 1>);
CASE(<assignments list 2>);
...
}
For example, a valid test case definition is CASE(N = 100, M = 75)
. Vectors and matrices can be assigned conveniently using braces; for example, CASE(V = {1, 2, 3}, M = {{1, 2}, {3, 4}})
.
For more complex assignment, such as constructing a tree, create a private void
method that constructs the tree, and call it in CASE()
. For example, CASE(N = 100, linearTree())
and having this private method:
/* Constructs a linear tree */
void linearTree() {
parent.clear(); // important!
for (int i = 0; i < N; i++) {
parent.push_back(i-1);
}
}
Note
The values of input variables before each test case are undefined, and may contain leftover computation from previous test cases. For example, for vectors, make sure that you initialize or clear them before adding values, as we did in the above example!
Each test case definition will be realized as a pair of input/output files, with the following filenames: <slug>_<case-number>.in
and <slug>_<case-number>.out
. For example, aplusb_1.in
, aplusb_1.out
.
Sample test case definition¶
Unlike test cases, sample test cases are specified as literal strings, exactly as they appear in problem statement. This way, you can be visually sure that you are typing sample test cases in problem statement correctly.
The literal strings are passed line-by-line to the SAMPLE_CASE()
macro as follows (note the {}
braces):
void SampleTestCases() {
// Sample test case 1
SAMPLE_CASE({
"<line 1>",
"<line 2>",
...
});
// Sample test case 2
SAMPLE_CASE({
"<line 1>",
"<line 2>",
...
});
...
}
For example, these sample test cases:
3 4
1 2 3
and
6 2
10 1 4 5 7 3
can be translated to:
void SampleTestCases() {
SAMPLE_CASE({
"3 4",
"1 2 3"
});
SAMPLE_CASE({
"6 2",
"10 1 4 5 7 3"
});
...
}
Each sample test case definition will be realized as a pair of input/output files, with the following filenames: <slug>_sample_<case-number>.in
and <slug>_sample_<case-number>.out
. For example, aplusb_sample_1.in
, aplusb_sample_1.out
.
Random number generator¶
tcframe provides a simple random number generator object, tcframe::rnd
. For example, you can use it to generate a random array: CASE(N = 100, randomArray())
where randomArray()
is defined as follows.
void SampleTestCases() {
A.clear();
for (int i = 0; i < N; i++) {
A.push_back(rnd.nextInt(1000000));
}
}
For more details, consult the API reference for random number generator.
Manipulating input variables with different representation¶
Often, you want to manipulate input variables with a different representation from what is defined in the input format section. For example, suppose that you want to have a tree as an input. In the input format (in problem specification class), you specify the tree as a list of edges (U[i], V[i]) as follows:
void InputFormat() {
LINE(N);
LINES(U, V) % SIZE(N - 1);
}
and you want to manipulate the tree as a vector P[], where P[i] is the parent of node i. (I.e., you have private variable vector<int> P
in generator specification class.)
This can be achieved by writing a special method FinalizeInput()
in generator specification class and transforming the vector P[] into a pair of vectors (U[], V[]) in it.
void FinalizeInput() {
U.clear();
P.clear();
for (int i = 0; i < N; i++) {
if (P[i] != -1) {
U.push_back(i);
V.push_back(P[i]);
}
}
}
Notes¶
Due to how currently the CASE()
macro is implemented, it is not possible for it to refer to a for-loop counter. For example, this is illegal:
void TestCases() {
for (int i = 1; i <= 100; i++) {
CASE(N = i);
}
}
In fact, for now please do not use any block constructs at all inside TestCases()
. The CASE()
macros should be first-level statements.
This may change in future versions.
Test groups¶
A test group is a set of test cases that are assigned/included to the same set of subtasks. If your problem has subtasks, then instead of writing TestCases()
method, you will be writing TestGroupX()
methods instead, where X
is the test group number.
For the whole test cases to be strong, a test case should be assigned in multiple subtasks if possible. In other words, several subtasks may share some test cases. In tcframe, a test case must be assigned to a subtask if (and only if) it satisfies the constraints of that subtask.
Designing test groups¶
In this example, we will use this subtasking scheme:
Our advice on designing test groups is as follows.
First, create a Venn diagram denoting the valid test cases for all subtasks. For this problem, the diagram will be like this:

In order to have a strong set of test cases, we should create a test group for each closed region in the Venn diagram. In this case, we will have four test groups as follows:
- Test group 1: consists of only one test case N = K = 1. Assign it to subtasks {1, 2, 3}.
- Test group 2: generate test cases that satisfy N = 1; 2 ≤ K ≤ 100. Assign them to subtasks {1, 3}.
- Test group 3: generate test cases that satisfy 2 ≤ N ≤ 100; K = 1. Assign them to subtasks {2, 3}.
- Test group 4: generate test cases that satisfy 2 ≤ N, K ≤ 100. Assign them to subtasks {3}.
Test group definitions¶
To define test groups, write each of the methods TestGroupX()
, where X
is a positive integer denoting the test group number, starting from 1. Then, call the assignToSubtasks(S)
method as the first statement, where S
is a list of subtask numbers. The remaining bodies of test group methods are test case definitions. For the above example:
void TestGroup1() {
assignToSubtasks({1, 2, 3});
// CASE() calls follow
}
void TestGroup2() {
assignToSubtasks({1, 3});
// CASE() calls follow
}
void TestGroup3() {
assignToSubtasks({2, 3});
// CASE() calls follow
}
void TestGroup4() {
assignToSubtasks({3});
// CASE() calls follow
}
Note
As of this version, you can define up to 10 test groups: TestGroup1() .. TestGroup10().
Each test case definition will be realized as a pair of input/output files, with the following filenames: <slug>_<group-number>_<case-number>.in
and <slug>_<group-number>_<case-number>.out
. For example, aplusb_2_3.in
, aplusb_2_3.out
.
Notes¶
Although a test group may be assigned to several subtasks, tcframe will produce the test case files according to their test group and test case number as defined above. It is up to the implementation of the grader on how to choose which test cases to grade for a subtask. For example, if your grader only accepts a set of test cases for each subtask, you can duplicate each test case for every subtask it is assigned to.
For tcframe‘s submission simulation feature, each test case will be executed only once. The result is shared for every subtask it is assigned to.
Multiple test cases per file¶
tcframe supports ICPC-style problems as well. In this style, for each of the SampleTestCases()
, TestCases()
, and TestGroupX()
methods, the test cases will be combined into a single file. The file is prepended with a single line containing the number of test cases in it.
To write an ICPC-style test cases generator, first write the runner as usual, assuming that the problem not ICPC-style. Then, apply the following changes.
Problem specification class¶
Declare an integer input variable (e.g.,
T
) that will hold the number of test cases in a single file.protected: int T; ...
In the problem configuration, call the
setMultipleTestCasesCount()
method with the previous variable as the argument.void Config() { ... setMultipleTestCasesCount(T); ... }
The input format specification should not contain
T
. It should specify the format of a single test case only. The number of test cases will be automatically prepended in the final combined test case input file.Constraints can be imposed on
T
, by writing the methodMultipleTestCasesConstraints()
method and defining constraints there.void MultipleTestCasesConstraints() { CONS(1 <= T && T <= 20); }
Generation specification class¶
No changes are necessary.
Solution program¶
Although the input format only specifies a single test case, the solution should read the number of test cases in the first line. In other words, the solution will read the final combined test cases input file.
Notes¶
Currently, it is impossible to prepend each output with something like Case #X
, where X
is the test case number.
Submission simulation¶
tcframe allows you to simulate submission locally, on your machine.
Before simulating a submission, you must have generated the test cases:
./runner
Then, you can “submit” a solution, by executing:
./runner submit [<submissionCommand>]
where <submissionCommand> is the command for executing the solution. If it is omitted, then the command will be the original solution command used in the generator.
For example, suppose you have written a generator for a problem. Your friend also has written an alternate solution to the problem, and he wants to check whether his solution agrees with yours. Let’s assume that his solution file is alt_solution.cpp
. Compile it into alt_solution
, place it in the same directory of the runner, and then run
./runner submit ./alt_solution
The verdict of each test case will be shown. The verdict will be one of the following:
- Accepted (AC)
- The output produced by the submission matches.
- Wrong Answer (WA)
- The output produced by the submission does not match. The diff will be shown, truncated to the first 10 lines.
- Runtime Error (RTE)
- The solution crashed or used memory above the limit, if specified.
- Time Limit Exceeded (TLE)
- The solution did not stop within the time limit, if specified.
The verdict of each subtask will be also shown. The verdict of a subtask is the worst verdict of all verdicts of test cases that are assigned to it. Here, RTE is worse than WA, and WA is worse than AC.
Here is a sample output of a submission for problems with subtasks.
Submitting...
[ SAMPLE TEST CASES ]
k-product_sample_1: Accepted
[ TEST GROUP 1 ]
k-product_1_1: Accepted
[ TEST GROUP 2 ]
k-product_2_1: Accepted
k-product_2_2: Accepted
k-product_2_3: Accepted
[ TEST GROUP 3 ]
k-product_3_1: Accepted
k-product_3_2: Wrong Answer
* Diff:
(expected) [line 01] 11
(received) [line 01] 12
k-product_3_3: Accepted
[ TEST GROUP 4 ]
k-product_4_1: Accepted
k-product_4_2: Accepted
k-product_4_3: Accepted
k-product_4_4: Accepted
k-product_4_5: Accepted
k-product_4_6: Runtime Error
* Execution of submission failed:
- Exit code: 1
- Standard error:
[ RESULT ]
Subtask 1: Accepted
Subtask 2: Wrong Answer
Subtask 3: Runtime Error
and here is for problems without subtasks
Submitting...
[ SAMPLE TEST CASES ]
k-product_sample_1: Accepted
[ OFFICIAL TEST CASES ]
k-product_1: Accepted
k-product_2: Accepted
k-product_3: Accepted
k-product_4: Wrong Answer
* Diff:
(expected) [line 01] 11
(received) [line 01] 12
[ RESULT ]
Wrong Answer
This submission feature is useful for creating “unit tests” for your test cases. For each problem, you can write many solutions with different intended results. For example, solution_123.cpp
should pass subtasks 1 - 3; solution_12.cpp
should pass subtasks 1 and 2 but not subtask 3, etc.
Brief output¶
If you want to automate checking the result of each solution, you can set the output of the submission to be “brief”, i.e., concise and easy to parse by another program. Just pass the command-line option --brief
:
./runner submit ./alt_solution --brief
Here is a sample brief output for problems with subtasks:
1:AC
2:WA
3:RTE
And here is for problems without subtasks:
WA
API Reference¶
- Problem configuration
- Input/output variables
- Input/output segments
- Constraints
- Generator configuration
- Test cases
- Random number generator
- Command line options
Problem configuration¶
The following methods are available inside the overridden method BaseProblem::Config()
.
-
void
setSlug
(string slug)¶ Sets the slug of the problem. A slug is a nickname or code for the problem. The produced test case filenames will have the slug as prefix. For example, if the slug is “helloworld” then one valid test case filename is “helloworld_1.in”.
If not specified, the default slug is
"problem"
.
-
void
setTimeLimit
(int timeLimitInSeconds)¶ Sets the time limit of the problem, in seconds. This time limit is used in submission simulation.
-
void
setMemoryLimit
(int memoryLimitInMegabytes)¶ Sets the memory limit of the problem, in MB. This memory limit is used in submission simulation.
-
void
setMultipleTestCasesCount
(int &countVariable)¶ Sets countVariable to hold the total number of test cases in a single file, in multiple test cases per file problems.
Input/output variables¶
There are three supported types of variables:
- Scalar
- Variables of built-in integral types (
int
,long long
,char
, etc.), built-in floating-point types (float
,double
), andstd::string
. - Vector
std::vector<T>
, whereT
is a scalar type as defined above. Arrays (T[]
) are not supported.- Matrix
std::vector<std::vector<T>>
, whereT
is a scalar type as defined above. 2D arrays (T[][]
) are not supported.
Input/output segments¶
The following macros are available inside the overridden method BaseProblem::InputFormat()
and BaseProblem::OutputFormat()
.
-
EMPTY_LINE
()¶ Defines an empty line.
-
LINE
(comma-separated elements)¶ Defines a single line containing space-separated scalar or vector variables. In case of vector variables, the elements are separated by spaces as well.
element is one of:
- <scalar variable name>.
- <vector variable name> % SIZE(<number of elements>). The number of elements can be a constant or a scalar variable.
- <vector variable name>. Here, the number of elements is unspecified. This kind of element must occur last in a line segment, if any. Elements will be considered until new line is found.
For example:
void InputFormat() { LINE(N); LINE(A % SIZE(3), B); LINE(M, C % SIZE(M)); }
With N = 2, A = {1, 2, 3}, B = {100, 200, 300, 400}, M = 2, C = {7, 8}, the above segments will produce:
2 1 2 3 100 200 300 400 2 7 8
-
LINES
(comma-separated vector/matrix variable names) % SIZE(number of elements)¶ Defines multiple lines, each consisting space-separated elements of given vector/matrix variables.
For example:
void InputFormat() { LINES(V) % SIZE(2); LINES(X, Y) % SIZE(N); }
With V = {1, 2}, X = {100, 110, 120}, Y = {200, 210, 220}, N = 3, the above segments will produce:
1 2 100 200 110 210 120 220
If a matrix variable is given, it must occur as the last argument, and the number of rows must match with the number of elements of the other vector variables (if any). It is not required that each row of the matrix consists of the same number of columns.
For example:
void InputFormat() { LINES(op, data) % SIZE(2); }
With op = {“UPDATE, “QUERY”}, data = {{3, 5}, {7}}, the above segments will produce:
UPDATE 3 5 QUERY 7
-
GRID
(matrix variable name) % SIZE(number of rows, number of columns)¶ Defines a grid consisting elements of a given matrix variable. If the given matrix variable is of type char, the elements in each row is not space-separated, otherwise they are space-separated.
For example:
void InputFormat() { GRID(G) % SIZE(2, 2); GRID(H) % SIZE(R, C); }
With G = {{‘a’, ‘b’}, {‘c’, ‘d’}}, H = {{1, 2, 3}, {4, 5, 6}}, R = 2, C = 3, the above segments will produce:
ab cd 1 2 3 4 5 6
Constraints¶
The following macros are available inside the overridden method BaseProblem::Constraints()
, BaseProblem::MultipleTestCasesConstraints()
, and BaseProblem::SubtaskX()
.
-
CONS
(predicate)¶ Defines a constraint. predicate is a boolean expression, whose value must be completely determined by the values of the input variables (only).
For example:
void Subtask1() { CONS(A <= B && B <= 1000); CONS(graphDoesNotHaveCycles()); }
Generator configuration¶
The following methods are available inside the overridden method BaseGenerator::Config()
.
-
void
setTestCasesDir
(string testCasesDir)¶ Sets the directory for the generated test case files, relative to the location of the generator program.
If not specified, the default directory is
"tc"
.
-
void
setSolutionCommand
(string solutionCommand)¶ Sets the command for executing the official solution. This will be used for generating test case output files. For each input files, this will be executed:
solutionCommand < [input filename] > [output filename]
If not specified, the default solution command is
"./solution"
.
Test cases¶
The following macros are available inside the overridden method BaseGenerator::TestCases()
.
-
void
assignToSubtasks
(set<int> subtaskNumbers)¶ Assigns the current test test group to a set of subtasks.
For example:
void TestGroup1() { assignToSubtasks({1, 3}); // test case definitions follow }
The following macros are available inside the overridden method BaseGenerator::TestCases()
and BaseGenerator::TestGroupX()
.
-
CASE
(comma-separated statements)¶ Defines a test case.
statement should be one of:
- assignment to an input variables
- private method call that assigns values to one or more input variables
For example:
void TestCases() { CASE(N = 42, M = 100, randomArray()); CASE(N = 1000, M = 1000, randomArray()); }
The following macros are available inside the overridden method BaseGenerator::SampleTestCases()
.
-
SAMPLE_CASE
(list of lines[, list of subtask numbers])¶ Defines a sample test case. A sample test case is defined as an exact literal string, given as list of lines. list of subtask numbers are only valid in problems with subtasks.
For example, to define this sample test case:
1 2 3 4 5
You can do this way:
void SampleTestCases() { SAMPLE_CASE({ "1 2", "3 4 5" }); }
for problems without subtasks. For problems with subtasks:
void SampleTestCases() { SAMPLE_CASE({ "1 2", "3 4 5" }, {1, 3}); }
assuming that the sample test case is assigned to subtasks 1 and 3.
Multiple sample test cases can be defined inside the same method.
Random number generator¶
The following methods are available on the random number generator rnd
object inside a generator.
-
int
nextInt
(int minNum, int maxNum)¶ Returns a uniformly distributed random integer (int) between minNum and maxNum, inclusive.
-
int
nextInt
(int maxNumEx)¶ Returns a uniformly distributed random integer (int) between 0 and maxNumEx - 1, inclusive.
-
long long
nextLongLong
(long long minNum, long long maxNum)¶ Returns a uniformly distributed random integer (long long) between minNum and maxNum, inclusive.
-
long long
nextLongLong
(long long maxNumEx)¶ Returns a uniformly distributed random integer (long long) between 0 and maxNumEx - 1, inclusive.
-
double
nextDouble
(double minNum, double maxNum)¶ Returns a uniformly distributed random real number (double) between minNum and maxNum, inclusive.
-
double
nextDouble
(double maxNum)¶ Returns a uniformly distributed random real number (double) between 0 and maxNum, inclusive.
-
void
shuffle
(std::RandomAccessIterator first, std::RandomAccessIterator last)¶ Randomly shuffles the elements in [first, last). Use this rather than
std::random_shuffle()
.
Command-line options¶
The following options can be specified when running the runner program. They mostly override the specified problem and generator configuration.
-
--slug=slug
Overrides the slug specified by
setSlug()
in problem configuration.
-
--tc-dir=dir
Overrides the test cases directory specified by
setTestCasesDir()
in generator configuration.
-
--solution-command=command
Overrides the solution command specified by
setSolutionCommand()
in generator configuration.
-
--seed=seed
Sets the seed for the random number generator
rnd
inside the generator.
-
--time-limit=timeLimitInSeconds
Overrides the time limit specified by
setTimeLimit()
in problem configuration.
-
--memory-limit=memoryLimitInMegabytes
Overrides the memory limit specified by
setMemoryLimit()
in problem configuration.
-
--no-time-limit
Unsets the time limit specified by
setTimeLimit()
in problem configuration.
-
--no-memory-limit
Unsets the memory limit specified by
setMemoryLimit()
in problem configuration.