1 module unittester;
2 
3 import tango.core.ThreadPool;
4 import tango.io.Stdout;
5 import Path = tango.io.Path;
6 import tango.io.FilePath;
7 import tango.io.device.File;
8 import tango.text.Arguments;
9 import tango.sys.Process;
10 import tango.core.sync.Condition;
11 import tango.core.sync.Mutex;
12 
13 enum immutable(char)[] DummyFileContent = "void main() {}";
14 enum immutable(char)[] Help = 
15 `
16 unittester - TangoD2's unittest utility
17 Copyright (C) 2012-2014 Pavel Sountsov.
18 
19 Usage: unittester [OPTION]... [FILES]...
20 Example: unittester -c dmd -d .unittest -a "-m32" tango/text/Util.d
21 
22 FILES is a list of files you want to run unittests in. This program essentially
23 compiles each of the passed files individually along with a dummy main file.
24 Then it runs the resultant executable to see if it's unittests run.
25 Additionally, if the unittest runs longer than 5 seconds, it is aborted.
26 
27 Options:
28   -a,  --additional=OPTIONS   what additional options to pass the compiler. 
29                               These are appended after the options specified by
30                               the --options option.
31   -c,  --compiler=COMPILER    what compiler to use when compiling the unittests
32                               Default: dmd
33   -d,  --directory=DIRECTORY  what directory to use to write the test 
34                               executables to. Note that this directory is not
35                               removed after the program finishes.
36                               Default: .unittest
37   -h,  --help                 print this help text
38   -o,  --options=OPTIONS      what options to pass to the compiler.
39                               Default if compiler is dmd: 
40                                   -unittest -L-ltango -debug=UnitTest
41                               Default if compiler is ldc2: 
42                                   -unittest -L-ltango -d-debug=UnitTest
43 `;
44 
45 void main(const(char[])[] args)
46 {
47 	const(char)[] compiler = "dmd";
48 	const(char)[] compiler_options;
49 	const(char)[] directory = ".unittest";
50 	const(char)[] additional_options;
51 	
52 	const(char)[] null_str = null; /* DMD bug*/
53 	
54 	auto arguments = new Arguments;
55 	arguments("compiler").aliased('c').params(1).bind(
56 		(const(char)[] arg)
57 		{
58 			compiler = arg;
59 			return null_str;
60 		});
61 	arguments("options").aliased('o').params(1).bind(
62 		(const(char)[] arg)
63 		{
64 			compiler_options = arg;
65 			return null_str;
66 		});
67 	arguments("additional").aliased('a').params(1).bind(
68 		(const(char)[] arg)
69 		{
70 			additional_options = arg;
71 			return null_str;
72 		});
73 	arguments("directory").aliased('d').params(1).bind(
74 		(const(char)[] arg)
75 		{
76 			directory = arg;
77 			return null_str;
78 		});
79 	arguments("help").aliased('h').halt();
80 	arguments(null).params(0, 1024);
81 	
82 	if(!arguments.parse(args[1..$]) || arguments("help").set)
83 	{
84 		Stdout(arguments.errors(&Stdout.layout.sprint));
85 		return;
86 	}
87 	
88 	if(compiler_options == "")
89 	{
90 		version(Windows)
91 		{
92 			compiler_options = "-unittest libtango-dmd.lib -debug=UnitTest";
93 		}
94 		else
95 		{
96 			auto compiler_path = Path.parse(compiler);
97 			switch(compiler_path.name)
98 			{
99 				case "dmd":
100 					compiler_options = "-unittest -L-ltango-dmd -debug=UnitTest";
101 					break;
102 				case "ldc2":
103 					compiler_options = "-unittest -L-ltango-ldc -d-debug=UnitTest";
104 					break;
105 				default:
106 					assert(0, "Unsupported compiler.");
107 			}
108 		}
109 	}
110 	
111 	/*
112 	 * Set up files and directories
113 	 */
114 	if(!Path.exists(directory))
115 		Path.createFolder(directory);
116 	
117 	auto dummy_file_fp = new FilePath(directory.dup);
118 	if(dummy_file_fp.isFile)
119 	{
120 		throw new Exception("'" ~ directory.idup ~ "' already exists and is a file!");
121 	}
122 	dummy_file_fp.append("dummy.d");
123 	dummy_file_fp.native;
124 	
125 	{
126 		auto dummy_file = new File(dummy_file_fp.cString()[0..$-1], File.WriteCreate);
127 		scope(exit) dummy_file.close();
128 		dummy_file.write(DummyFileContent);
129 	}
130 	
131 	/*
132 	 * Set up compiler business
133 	 */
134 	auto output_fp = new FilePath(directory.dup);
135 	output_fp.append("out");
136 	output_fp.native;
137 	auto proc_raw_arguments = compiler ~ " " ~ compiler_options ~ " " ~ 
138 	                          additional_options ~ " -of" ~ output_fp.cString()[0..$-1] ~ " " ~ 
139 	                          dummy_file_fp.cString()[0..$-1];
140 	auto compiler_proc = new Process(true, proc_raw_arguments);
141 	compiler_proc.setRedirect(Redirect.All | Redirect.ErrorToOutput);
142 	auto proc_arguments = compiler_proc.args().dup;
143 	
144 	auto test_proc = new Process(true, output_fp.cString()[0..$-1]);
145 	test_proc.setRedirect(Redirect.All | Redirect.ErrorToOutput);
146 	
147 	auto test_mutex = new Mutex;
148 	auto test_condition = new Condition(test_mutex);
149 	
150 	auto thread_pool = new ThreadPool!()(1);
151 	
152 	size_t skipped = 0;
153 	size_t total = 0;
154 	size_t pass = 0;
155 	size_t fail = 0;
156 	size_t timeout = 0;
157 	size_t compile_error = 0;
158 	
159 	foreach(file; arguments(null).assigned())
160 	{
161 		if(Path.parse(file).ext != "d")
162 		{
163 			Stdout.formatln("Skipping '{}'... Not a D file.", file);
164 			skipped++;
165 			continue;
166 		}
167 		if(!Path.exists(file))
168 		{
169 			Stdout.formatln("Skipping '{}'... Doesn't exist.", file);
170 			skipped++;
171 			continue;
172 		}
173 		
174 		total++;
175 		
176 		Stdout.formatln("Compiling '{}'", file);
177 		
178 		auto file_fp = new FilePath(file.dup);
179 		file_fp.native;
180 		auto new_args = proc_arguments ~ file_fp.cString()[0..$-1];
181 		compiler_proc.setArgs(compiler, new_args).execute();
182 		Stdout.copy(compiler_proc.stdout).flush();
183 		auto result = compiler_proc.wait();
184 		
185 		if(result.status != 0)
186 		{
187 			Stdout("COMPILEERROR").nl;
188 			Stdout(result.toString()).nl;
189 			compile_error++;
190 			continue;
191 		}
192 		
193 		Stdout("Testing...").nl;
194 
195 		Process.Result test_result;
196 		bool killed;
197 
198 		void test_thread()
199 		{
200 			synchronized(test_mutex)
201 			{
202 				if(!test_condition.wait(10))
203 				{
204 					killed = true;
205 					test_proc.kill();
206 				}
207 			}
208 		}
209 
210 		thread_pool.assign(&test_thread);
211 		
212 		test_proc.execute();
213 		Stdout.copy(test_proc.stdout).flush();
214 		test_result = test_proc.wait();
215 		
216 		test_condition.notify();
217 		
218 		thread_pool.wait();
219 		
220 		if(test_result.reason == Process.Result.Exit && test_result.status == 0)
221 		{
222 			Stdout("PASS").nl;
223 			pass++;
224 		}
225 		else if(test_result.reason == Process.Result.Signal && killed)
226 		{
227 			Stdout("TIMEDOUT").nl;
228 			Stdout(result.toString()).nl;
229 			timeout++;
230 		}
231 		else
232 		{
233 			Stdout("FAIL").nl;
234 			Stdout(result.toString()).nl;
235 			fail++;
236 		}
237 	}
238 	
239 	Stdout.nl;
240 	Stdout.formatln("{} tested. {} skipped.", total, skipped);
241 	Stdout.formatln("PASS: {}", pass);
242 	Stdout.formatln("FAIL: {}", fail);
243 	Stdout.formatln("TIMEDOUT: {}", timeout);
244 	Stdout.formatln("COMPILEERROR: {}", compile_error);
245 }