OLD | NEW |
1 // Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 // Creates an instance of the test_shell. | 5 // Creates an instance of the test_shell. |
6 | 6 |
7 #include <stdlib.h> // required by _set_abort_behavior | 7 #include <stdlib.h> // required by _set_abort_behavior |
8 | 8 |
9 #include <windows.h> | 9 #include <windows.h> |
10 #include <commctrl.h> | 10 #include <commctrl.h> |
11 | 11 |
12 #include "base/at_exit.h" | 12 #include "base/at_exit.h" |
13 #include "base/basictypes.h" | 13 #include "base/basictypes.h" |
14 #include "base/command_line.h" | 14 #include "base/command_line.h" |
15 #include "base/event_recorder.h" | 15 #include "base/event_recorder.h" |
16 #include "base/file_util.h" | 16 #include "base/file_util.h" |
17 #include "base/gfx/native_theme.h" | 17 #include "base/gfx/native_theme.h" |
18 #include "base/icu_util.h" | 18 #include "base/icu_util.h" |
19 #include "base/memory_debug.h" | 19 #include "base/memory_debug.h" |
20 #include "base/message_loop.h" | 20 #include "base/message_loop.h" |
21 #include "base/path_service.h" | 21 #include "base/path_service.h" |
| 22 #include "base/process_util.h" |
22 #include "base/resource_util.h" | 23 #include "base/resource_util.h" |
23 #include "base/stack_container.h" | 24 #include "base/stack_container.h" |
24 #include "base/stats_table.h" | 25 #include "base/stats_table.h" |
25 #include "base/string_util.h" | 26 #include "base/string_util.h" |
26 #include "base/trace_event.h" | 27 #include "base/trace_event.h" |
27 #include "breakpad/src/client/windows/handler/exception_handler.h" | 28 #include "breakpad/src/client/windows/handler/exception_handler.h" |
28 #include "net/base/cookie_monster.h" | 29 #include "net/base/cookie_monster.h" |
29 #include "net/base/net_module.h" | 30 #include "net/base/net_module.h" |
30 #include "net/http/http_cache.h" | 31 #include "net/http/http_cache.h" |
31 #include "net/http/http_network_layer.h" | 32 #include "net/http/http_network_layer.h" |
(...skipping 72 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
104 newPath->append(L".dmp"); | 105 newPath->append(L".dmp"); |
105 | 106 |
106 // May use the heap, but oh well. If this fails, we'll just have the | 107 // May use the heap, but oh well. If this fails, we'll just have the |
107 // original dump file lying around. | 108 // original dump file lying around. |
108 _wrename(origPath->c_str(), newPath->c_str()); | 109 _wrename(origPath->c_str(), newPath->c_str()); |
109 | 110 |
110 return false; | 111 return false; |
111 } | 112 } |
112 } // namespace | 113 } // namespace |
113 | 114 |
114 int main(int argc, char* argv[]) | 115 int main(int argc, char* argv[]) { |
115 { | 116 process_util::EnableTerminationOnHeapCorruption(); |
116 #ifdef _CRTDBG_MAP_ALLOC | 117 #ifdef _CRTDBG_MAP_ALLOC |
117 _CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STDERR); | 118 _CrtSetReportFile(_CRT_WARN, _CRTDBG_FILE_STDERR); |
118 _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE); | 119 _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_FILE); |
119 #endif | 120 #endif |
120 // Some tests may use base::Singleton<>, thus we need to instanciate | 121 // Some tests may use base::Singleton<>, thus we need to instanciate |
121 // the AtExitManager or else we will leak objects. | 122 // the AtExitManager or else we will leak objects. |
122 base::AtExitManager at_exit_manager; | 123 base::AtExitManager at_exit_manager; |
123 | 124 |
124 CommandLine parsed_command_line; | 125 CommandLine parsed_command_line; |
125 if (parsed_command_line.HasSwitch(test_shell::kStartupDialog)) | 126 if (parsed_command_line.HasSwitch(test_shell::kStartupDialog)) |
126 MessageBox(NULL, L"attach to me?", L"test_shell", MB_OK); | 127 MessageBox(NULL, L"attach to me?", L"test_shell", MB_OK); |
127 //webkit_glue::SetLayoutTestMode(true); | 128 //webkit_glue::SetLayoutTestMode(true); |
128 | 129 |
129 // Allocate a message loop for this thread. Although it is not used | 130 // Allocate a message loop for this thread. Although it is not used |
130 // directly, its constructor sets up some necessary state. | 131 // directly, its constructor sets up some necessary state. |
131 MessageLoopForUI main_message_loop; | 132 MessageLoopForUI main_message_loop; |
132 | 133 |
133 bool suppress_error_dialogs = | 134 bool suppress_error_dialogs = |
134 (GetEnvironmentVariable(L"CHROME_HEADLESS", NULL, 0) || | 135 (GetEnvironmentVariable(L"CHROME_HEADLESS", NULL, 0) || |
135 parsed_command_line.HasSwitch(test_shell::kNoErrorDialogs) || | 136 parsed_command_line.HasSwitch(test_shell::kNoErrorDialogs) || |
136 parsed_command_line.HasSwitch(test_shell::kLayoutTests)); | 137 parsed_command_line.HasSwitch(test_shell::kLayoutTests)); |
137 TestShell::InitLogging(suppress_error_dialogs); | 138 TestShell::InitLogging(suppress_error_dialogs); |
138 | 139 |
139 // Suppress abort message in v8 library in debugging mode. | 140 // Suppress abort message in v8 library in debugging mode. |
140 // V8 calls abort() when it hits assertion errors. | 141 // V8 calls abort() when it hits assertion errors. |
141 if (suppress_error_dialogs) { | 142 if (suppress_error_dialogs) { |
142 _set_abort_behavior(0, _WRITE_ABORT_MSG); | 143 _set_abort_behavior(0, _WRITE_ABORT_MSG); |
143 } | 144 } |
144 | 145 |
145 if (parsed_command_line.HasSwitch(test_shell::kEnableTracing)) | 146 if (parsed_command_line.HasSwitch(test_shell::kEnableTracing)) |
146 base::TraceLog::StartTracing(); | 147 base::TraceLog::StartTracing(); |
147 | 148 |
148 // Make the selection of network stacks early on before any consumers try to | 149 // Make the selection of network stacks early on before any consumers try to |
149 // issue HTTP requests. | 150 // issue HTTP requests. |
150 if (parsed_command_line.HasSwitch(test_shell::kUseNewHttp)) | 151 if (parsed_command_line.HasSwitch(test_shell::kUseNewHttp)) |
151 net::HttpNetworkLayer::UseWinHttp(false); | 152 net::HttpNetworkLayer::UseWinHttp(false); |
152 | 153 |
153 bool layout_test_mode = | 154 bool layout_test_mode = |
154 parsed_command_line.HasSwitch(test_shell::kLayoutTests); | 155 parsed_command_line.HasSwitch(test_shell::kLayoutTests); |
155 | 156 |
156 net::HttpCache::Mode cache_mode = net::HttpCache::NORMAL; | 157 net::HttpCache::Mode cache_mode = net::HttpCache::NORMAL; |
157 bool playback_mode = | 158 bool playback_mode = |
158 parsed_command_line.HasSwitch(test_shell::kPlaybackMode); | 159 parsed_command_line.HasSwitch(test_shell::kPlaybackMode); |
159 bool record_mode = | 160 bool record_mode = |
160 parsed_command_line.HasSwitch(test_shell::kRecordMode); | 161 parsed_command_line.HasSwitch(test_shell::kRecordMode); |
161 | 162 |
162 if (playback_mode) | 163 if (playback_mode) |
163 cache_mode = net::HttpCache::PLAYBACK; | 164 cache_mode = net::HttpCache::PLAYBACK; |
164 else if (record_mode) | 165 else if (record_mode) |
165 cache_mode = net::HttpCache::RECORD; | 166 cache_mode = net::HttpCache::RECORD; |
166 | 167 |
167 if (layout_test_mode || | 168 if (layout_test_mode || |
168 parsed_command_line.HasSwitch(test_shell::kEnableFileCookies)) | 169 parsed_command_line.HasSwitch(test_shell::kEnableFileCookies)) |
169 net::CookieMonster::EnableFileScheme(); | 170 net::CookieMonster::EnableFileScheme(); |
170 | 171 |
171 std::wstring cache_path = | 172 std::wstring cache_path = |
172 parsed_command_line.GetSwitchValue(test_shell::kCacheDir); | 173 parsed_command_line.GetSwitchValue(test_shell::kCacheDir); |
173 if (cache_path.empty()) { | 174 if (cache_path.empty()) { |
174 PathService::Get(base::DIR_EXE, &cache_path); | 175 PathService::Get(base::DIR_EXE, &cache_path); |
175 file_util::AppendToPath(&cache_path, L"cache"); | 176 file_util::AppendToPath(&cache_path, L"cache"); |
176 } | 177 } |
177 | 178 |
178 // Initializing with a default context, which means no on-disk cookie DB, | 179 // Initializing with a default context, which means no on-disk cookie DB, |
179 // and no support for directory listings. | 180 // and no support for directory listings. |
180 SimpleResourceLoaderBridge::Init( | 181 SimpleResourceLoaderBridge::Init( |
181 new TestShellRequestContext(cache_path, cache_mode)); | 182 new TestShellRequestContext(cache_path, cache_mode)); |
182 | 183 |
183 // Load ICU data tables | 184 // Load ICU data tables |
184 icu_util::Initialize(); | 185 icu_util::Initialize(); |
185 | 186 |
186 // Config the network module so it has access to a limited set of resources. | 187 // Config the network module so it has access to a limited set of resources. |
187 net::NetModule::SetResourceProvider(NetResourceProvider); | 188 net::NetModule::SetResourceProvider(NetResourceProvider); |
188 | 189 |
189 INITCOMMONCONTROLSEX InitCtrlEx; | 190 INITCOMMONCONTROLSEX InitCtrlEx; |
190 | 191 |
191 InitCtrlEx.dwSize = sizeof(INITCOMMONCONTROLSEX); | 192 InitCtrlEx.dwSize = sizeof(INITCOMMONCONTROLSEX); |
192 InitCtrlEx.dwICC = ICC_STANDARD_CLASSES; | 193 InitCtrlEx.dwICC = ICC_STANDARD_CLASSES; |
193 InitCommonControlsEx(&InitCtrlEx); | 194 InitCommonControlsEx(&InitCtrlEx); |
194 | 195 |
195 bool interactive = !layout_test_mode; | 196 bool interactive = !layout_test_mode; |
196 TestShell::InitializeTestShell(interactive); | 197 TestShell::InitializeTestShell(interactive); |
197 | 198 |
198 if (parsed_command_line.HasSwitch(test_shell::kAllowScriptsToCloseWindows)) | 199 if (parsed_command_line.HasSwitch(test_shell::kAllowScriptsToCloseWindows)) |
199 TestShell::SetAllowScriptsToCloseWindows(); | 200 TestShell::SetAllowScriptsToCloseWindows(); |
200 | 201 |
201 // Disable user themes for layout tests so pixel tests are consistent. | 202 // Disable user themes for layout tests so pixel tests are consistent. |
202 if (!interactive) | 203 if (!interactive) |
203 gfx::NativeTheme::instance()->DisableTheming(); | 204 gfx::NativeTheme::instance()->DisableTheming(); |
204 | 205 |
205 if (parsed_command_line.HasSwitch(test_shell::kTestShellTimeOut)) { | 206 if (parsed_command_line.HasSwitch(test_shell::kTestShellTimeOut)) { |
206 const std::wstring timeout_str = parsed_command_line.GetSwitchValue( | 207 const std::wstring timeout_str = parsed_command_line.GetSwitchValue( |
207 test_shell::kTestShellTimeOut); | 208 test_shell::kTestShellTimeOut); |
208 int timeout_ms = static_cast<int>(StringToInt64(timeout_str.c_str())); | 209 int timeout_ms = static_cast<int>(StringToInt64(timeout_str.c_str())); |
209 if (timeout_ms > 0) | 210 if (timeout_ms > 0) |
210 TestShell::SetFileTestTimeout(timeout_ms); | 211 TestShell::SetFileTestTimeout(timeout_ms); |
211 } | 212 } |
212 | 213 |
213 // Initialize global strings | 214 // Initialize global strings |
214 TestShell::RegisterWindowClass(); | 215 TestShell::RegisterWindowClass(); |
215 | 216 |
216 // Treat the first loose value as the initial URL to open. | 217 // Treat the first loose value as the initial URL to open. |
217 std::wstring uri; | 218 std::wstring uri; |
218 | 219 |
219 // Default to a homepage if we're interactive. | 220 // Default to a homepage if we're interactive. |
220 if (interactive) { | 221 if (interactive) { |
221 PathService::Get(base::DIR_SOURCE_ROOT, &uri); | 222 PathService::Get(base::DIR_SOURCE_ROOT, &uri); |
222 file_util::AppendToPath(&uri, L"webkit"); | 223 file_util::AppendToPath(&uri, L"webkit"); |
223 file_util::AppendToPath(&uri, L"data"); | 224 file_util::AppendToPath(&uri, L"data"); |
224 file_util::AppendToPath(&uri, L"test_shell"); | 225 file_util::AppendToPath(&uri, L"test_shell"); |
225 file_util::AppendToPath(&uri, L"index.html"); | 226 file_util::AppendToPath(&uri, L"index.html"); |
226 } | 227 } |
227 | 228 |
228 if (parsed_command_line.GetLooseValueCount() > 0) { | 229 if (parsed_command_line.GetLooseValueCount() > 0) { |
229 CommandLine::LooseValueIterator iter = parsed_command_line.GetLooseValue
sBegin(); | 230 CommandLine::LooseValueIterator iter( |
230 uri = *iter; | 231 parsed_command_line.GetLooseValuesBegin()); |
231 } | 232 uri = *iter; |
232 | 233 } |
233 if (parsed_command_line.HasSwitch(test_shell::kCrashDumps)) { | 234 |
234 std::wstring dir = parsed_command_line.GetSwitchValue(test_shell::kCrash
Dumps); | 235 if (parsed_command_line.HasSwitch(test_shell::kCrashDumps)) { |
235 new google_breakpad::ExceptionHandler(dir, 0, &MinidumpCallback, 0, true
); | 236 std::wstring dir( |
236 } | 237 parsed_command_line.GetSwitchValue(test_shell::kCrashDumps)); |
237 | 238 new google_breakpad::ExceptionHandler(dir, 0, &MinidumpCallback, 0, true); |
238 std::wstring js_flags = | 239 } |
239 parsed_command_line.GetSwitchValue(test_shell::kJavaScriptFlags); | 240 |
240 // Test shell always exposes the GC. | 241 std::wstring js_flags = |
241 CommandLine::AppendSwitch(&js_flags, L"expose-gc"); | 242 parsed_command_line.GetSwitchValue(test_shell::kJavaScriptFlags); |
242 webkit_glue::SetJavaScriptFlags(js_flags); | 243 // Test shell always exposes the GC. |
243 | 244 CommandLine::AppendSwitch(&js_flags, L"expose-gc"); |
244 // load and initialize the stats table. | 245 webkit_glue::SetJavaScriptFlags(js_flags); |
245 StatsTable *table = new StatsTable(kStatsFile, kStatsFileThreads, kStatsFile
Counters); | 246 |
246 StatsTable::set_current(table); | 247 // load and initialize the stats table. |
247 | 248 StatsTable *table = new StatsTable(kStatsFile, kStatsFileThreads, kStatsFileCo
unters); |
248 TestShell* shell; | 249 StatsTable::set_current(table); |
249 if (TestShell::CreateNewWindow(uri, &shell)) { | 250 |
250 if (record_mode || playback_mode) { | 251 TestShell* shell; |
251 // Move the window to the upper left corner for consistent | 252 if (TestShell::CreateNewWindow(uri, &shell)) { |
252 // record/playback mode. For automation, we want this to work | 253 if (record_mode || playback_mode) { |
253 // on build systems where the script invoking us is a background | 254 // Move the window to the upper left corner for consistent |
254 // process. So for this case, make our window the topmost window | 255 // record/playback mode. For automation, we want this to work |
255 // as well. | 256 // on build systems where the script invoking us is a background |
256 ForegroundHelper::SetForeground(shell->mainWnd()); | 257 // process. So for this case, make our window the topmost window |
257 ::SetWindowPos(shell->mainWnd(), HWND_TOP, 0, 0, 600, 800, 0); | 258 // as well. |
258 // Tell webkit as well. | 259 ForegroundHelper::SetForeground(shell->mainWnd()); |
259 webkit_glue::SetRecordPlaybackMode(true); | 260 ::SetWindowPos(shell->mainWnd(), HWND_TOP, 0, 0, 600, 800, 0); |
| 261 // Tell webkit as well. |
| 262 webkit_glue::SetRecordPlaybackMode(true); |
| 263 } |
| 264 |
| 265 shell->Show(shell->webView(), NEW_WINDOW); |
| 266 |
| 267 if (parsed_command_line.HasSwitch(test_shell::kDumpStatsTable)) |
| 268 shell->DumpStatsTableOnExit(); |
| 269 |
| 270 bool no_events = parsed_command_line.HasSwitch(test_shell::kNoEvents); |
| 271 if ((record_mode || playback_mode) && !no_events) { |
| 272 std::wstring script_path = cache_path; |
| 273 // Create the cache directory in case it doesn't exist. |
| 274 file_util::CreateDirectory(cache_path); |
| 275 file_util::AppendToPath(&script_path, L"script.log"); |
| 276 if (record_mode) |
| 277 base::EventRecorder::current()->StartRecording(script_path); |
| 278 if (playback_mode) |
| 279 base::EventRecorder::current()->StartPlayback(script_path); |
| 280 } |
| 281 |
| 282 if (parsed_command_line.HasSwitch(test_shell::kDebugMemoryInUse)) { |
| 283 base::MemoryDebug::SetMemoryInUseEnabled(true); |
| 284 // Dump all in use memory at startup |
| 285 base::MemoryDebug::DumpAllMemoryInUse(); |
| 286 } |
| 287 |
| 288 // See if we need to run the tests. |
| 289 if (layout_test_mode) { |
| 290 webkit_glue::SetLayoutTestMode(true); |
| 291 |
| 292 // Set up for the kind of test requested. |
| 293 TestShell::TestParams params; |
| 294 if (parsed_command_line.HasSwitch(test_shell::kDumpPixels)) { |
| 295 // The pixel test flag also gives the image file name to use. |
| 296 params.dump_pixels = true; |
| 297 params.pixel_file_name = parsed_command_line.GetSwitchValue( |
| 298 test_shell::kDumpPixels); |
| 299 if (params.pixel_file_name.size() == 0) { |
| 300 fprintf(stderr, "No file specified for pixel tests"); |
| 301 exit(1); |
260 } | 302 } |
261 | 303 } |
262 shell->Show(shell->webView(), NEW_WINDOW); | 304 if (parsed_command_line.HasSwitch(test_shell::kNoTree)) { |
263 | 305 params.dump_tree = false; |
264 if (parsed_command_line.HasSwitch(test_shell::kDumpStatsTable)) | 306 } |
265 shell->DumpStatsTableOnExit(); | 307 |
266 | 308 if (uri.length() == 0) { |
267 bool no_events = parsed_command_line.HasSwitch(test_shell::kNoEvents); | 309 // Watch stdin for URLs. |
268 if ((record_mode || playback_mode) && !no_events) { | 310 char filenameBuffer[2048]; |
269 std::wstring script_path = cache_path; | 311 while (fgets(filenameBuffer, sizeof(filenameBuffer), stdin)) { |
270 // Create the cache directory in case it doesn't exist. | 312 char *newLine = strchr(filenameBuffer, '\n'); |
271 file_util::CreateDirectory(cache_path); | 313 if (newLine) |
272 file_util::AppendToPath(&script_path, L"script.log"); | 314 *newLine = '\0'; |
273 if (record_mode) | 315 if (!*filenameBuffer) |
274 base::EventRecorder::current()->StartRecording(script_path); | 316 continue; |
275 if (playback_mode) | 317 |
276 base::EventRecorder::current()->StartPlayback(script_path); | 318 SetCurrentTestName(filenameBuffer); |
| 319 |
| 320 if (!TestShell::RunFileTest(filenameBuffer, params)) |
| 321 break; |
277 } | 322 } |
278 | 323 } else { |
279 if (parsed_command_line.HasSwitch(test_shell::kDebugMemoryInUse)) { | 324 TestShell::RunFileTest(WideToUTF8(uri).c_str(), params); |
280 base::MemoryDebug::SetMemoryInUseEnabled(true); | 325 } |
281 // Dump all in use memory at startup | 326 |
282 base::MemoryDebug::DumpAllMemoryInUse(); | 327 shell->CallJSGC(); |
283 } | 328 shell->CallJSGC(); |
284 | 329 if (shell) delete shell; |
285 // See if we need to run the tests. | 330 } else { |
286 if (layout_test_mode) { | 331 MessageLoop::current()->Run(); |
287 webkit_glue::SetLayoutTestMode(true); | 332 } |
288 | 333 |
289 // Set up for the kind of test requested. | 334 // Flush any remaining messages. This ensures that any accumulated |
290 TestShell::TestParams params; | 335 // Task objects get destroyed before we exit, which avoids noise in |
291 if (parsed_command_line.HasSwitch(test_shell::kDumpPixels)) { | 336 // purify leak-test results. |
292 // The pixel test flag also gives the image file name to use. | 337 MessageLoop::current()->RunAllPending(); |
293 params.dump_pixels = true; | 338 |
294 params.pixel_file_name = parsed_command_line.GetSwitchValue( | 339 if (record_mode) |
295 test_shell::kDumpPixels); | 340 base::EventRecorder::current()->StopRecording(); |
296 if (params.pixel_file_name.size() == 0) { | 341 if (playback_mode) |
297 fprintf(stderr, "No file specified for pixel tests"); | 342 base::EventRecorder::current()->StopPlayback(); |
298 exit(1); | 343 } |
299 } | 344 |
300 } | 345 TestShell::ShutdownTestShell(); |
301 if (parsed_command_line.HasSwitch(test_shell::kNoTree)) { | 346 TestShell::CleanupLogging(); |
302 params.dump_tree = false; | 347 |
303 } | 348 // Tear down shared StatsTable; prevents unit_tests from leaking it. |
304 | 349 StatsTable::set_current(NULL); |
305 if (uri.length() == 0) { | 350 delete table; |
306 // Watch stdin for URLs. | |
307 char filenameBuffer[2048]; | |
308 while (fgets(filenameBuffer, sizeof(filenameBuffer), stdin)) { | |
309 char *newLine = strchr(filenameBuffer, '\n'); | |
310 if (newLine) | |
311 *newLine = '\0'; | |
312 if (!*filenameBuffer) | |
313 continue; | |
314 | |
315 SetCurrentTestName(filenameBuffer); | |
316 | |
317 if (!TestShell::RunFileTest(filenameBuffer, params)) | |
318 break; | |
319 } | |
320 } else { | |
321 TestShell::RunFileTest(WideToUTF8(uri).c_str(), params); | |
322 } | |
323 | |
324 shell->CallJSGC(); | |
325 shell->CallJSGC(); | |
326 if (shell) delete shell; | |
327 } else { | |
328 MessageLoop::current()->Run(); | |
329 } | |
330 | |
331 // Flush any remaining messages. This ensures that any accumulated | |
332 // Task objects get destroyed before we exit, which avoids noise in | |
333 // purify leak-test results. | |
334 MessageLoop::current()->RunAllPending(); | |
335 | |
336 if (record_mode) | |
337 base::EventRecorder::current()->StopRecording(); | |
338 if (playback_mode) | |
339 base::EventRecorder::current()->StopPlayback(); | |
340 } | |
341 | |
342 TestShell::ShutdownTestShell(); | |
343 TestShell::CleanupLogging(); | |
344 | |
345 // Tear down shared StatsTable; prevents unit_tests from leaking it. | |
346 StatsTable::set_current(NULL); | |
347 delete table; | |
348 | 351 |
349 #ifdef _CRTDBG_MAP_ALLOC | 352 #ifdef _CRTDBG_MAP_ALLOC |
350 _CrtDumpMemoryLeaks(); | 353 _CrtDumpMemoryLeaks(); |
351 #endif | 354 #endif |
352 return 0; | 355 return 0; |
353 } | 356 } |
354 | |
355 | |
OLD | NEW |