xref: /aosp_15_r20/external/deqp/executor/tools/xeCommandLineExecutor.cpp (revision 35238bce31c2a825756842865a792f8cf7f89930)
1 /*-------------------------------------------------------------------------
2  * drawElements Quality Program Test Executor
3  * ------------------------------------------
4  *
5  * Copyright 2014 The Android Open Source Project
6  *
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  *
19  *//*!
20  * \file
21  * \brief Command line test executor.
22  *//*--------------------------------------------------------------------*/
23 
24 #include "xeBatchExecutor.hpp"
25 #include "xeLocalTcpIpLink.hpp"
26 #include "xeTcpIpLink.hpp"
27 #include "xeTestCaseListParser.hpp"
28 #include "xeTestLogWriter.hpp"
29 #include "xeTestResultParser.hpp"
30 
31 #include "deCommandLine.hpp"
32 #include "deDirectoryIterator.hpp"
33 #include "deStringUtil.hpp"
34 #include "deUniquePtr.hpp"
35 
36 #include "deString.h"
37 
38 #include <algorithm>
39 #include <cstdio>
40 #include <cstdlib>
41 #include <fstream>
42 #include <iostream>
43 #include <memory>
44 #include <sstream>
45 #include <string>
46 #include <vector>
47 
48 #if (DE_OS == DE_OS_UNIX) || (DE_OS == DE_OS_ANDROID) || (DE_OS == DE_OS_WIN32)
49 #include <signal.h>
50 #endif
51 
52 using std::string;
53 using std::vector;
54 
55 namespace
56 {
57 
58 // Command line arguments.
59 namespace opt
60 {
61 
62 DE_DECLARE_COMMAND_LINE_OPT(StartServer, string);
63 DE_DECLARE_COMMAND_LINE_OPT(Host, string);
64 DE_DECLARE_COMMAND_LINE_OPT(Port, int);
65 DE_DECLARE_COMMAND_LINE_OPT(CaseListDir, string);
66 DE_DECLARE_COMMAND_LINE_OPT(TestSet, vector<string>);
67 DE_DECLARE_COMMAND_LINE_OPT(ExcludeSet, vector<string>);
68 DE_DECLARE_COMMAND_LINE_OPT(ContinueFile, string);
69 DE_DECLARE_COMMAND_LINE_OPT(TestLogFile, string);
70 DE_DECLARE_COMMAND_LINE_OPT(InfoLogFile, string);
71 DE_DECLARE_COMMAND_LINE_OPT(Summary, bool);
72 
73 // TargetConfiguration
74 DE_DECLARE_COMMAND_LINE_OPT(BinaryName, string);
75 DE_DECLARE_COMMAND_LINE_OPT(WorkingDir, string);
76 DE_DECLARE_COMMAND_LINE_OPT(CmdLineArgs, string);
77 
parseCommaSeparatedList(const char * src,vector<string> * dst)78 void parseCommaSeparatedList(const char *src, vector<string> *dst)
79 {
80     std::istringstream inStr(src);
81     string comp;
82 
83     while (std::getline(inStr, comp, ','))
84         dst->push_back(comp);
85 }
86 
registerOptions(de::cmdline::Parser & parser)87 void registerOptions(de::cmdline::Parser &parser)
88 {
89     using de::cmdline::NamedValue;
90     using de::cmdline::Option;
91 
92     static const NamedValue<bool> s_yesNo[] = {{"yes", true}, {"no", false}};
93 
94     parser << Option<StartServer>("s", "start-server", "Start local execserver. Path to the execserver binary.")
95            << Option<Host>("c", "connect", "Connect to host. Address of the execserver.")
96            << Option<Port>("p", "port", "TCP port of the execserver.", "50016")
97            << Option<CaseListDir>("cd", "caselistdir", "Path to the directory containing test case XML files.", ".")
98            << Option<TestSet>("t", "testset", "Comma-separated list of include filters.", parseCommaSeparatedList)
99            << Option<ExcludeSet>("e", "exclude", "Comma-separated list of exclude filters.", parseCommaSeparatedList,
100                                  "")
101            << Option<ContinueFile>(DE_NULL, "continue",
102                                    "Continue execution by initializing results from existing test log.")
103            << Option<TestLogFile>("o", "out", "Output test log filename.", "TestLog.qpa")
104            << Option<InfoLogFile>("i", "info", "Output info log filename.", "InfoLog.txt")
105            << Option<Summary>(DE_NULL, "summary", "Print summary after running tests.", s_yesNo, "yes")
106            << Option<BinaryName>("b", "binaryname", "Test binary path. Relative to working directory.", "<Unused>")
107            << Option<WorkingDir>("wd", "workdir", "Working directory for the test execution.", ".")
108            << Option<CmdLineArgs>(DE_NULL, "cmdline", "Additional command line arguments for the test binary.", "");
109 }
110 
111 } // namespace opt
112 
113 enum RunMode
114 {
115     RUNMODE_CONNECT,
116     RUNMODE_START_SERVER
117 };
118 
119 struct CommandLine
120 {
CommandLine__anona4f687020111::CommandLine121     CommandLine(void) : port(0), summary(false)
122     {
123     }
124 
125     xe::TargetConfiguration targetCfg;
126     RunMode runMode;
127     string serverBinOrAddress;
128     int port;
129     string caseListDir;
130     vector<string> testset;
131     vector<string> exclude;
132     string inFile;
133     string outFile;
134     string infoFile;
135     bool summary;
136 };
137 
parseCommandLine(CommandLine & cmdLine,int argc,const char * const * argv)138 bool parseCommandLine(CommandLine &cmdLine, int argc, const char *const *argv)
139 {
140     de::cmdline::Parser parser;
141     de::cmdline::CommandLine opts;
142 
143     XE_CHECK(argc >= 1);
144 
145     opt::registerOptions(parser);
146 
147     if (!parser.parse(argc - 1, argv + 1, &opts, std::cerr))
148     {
149         std::cout << argv[0] << " [options]\n";
150         parser.help(std::cout);
151         return false;
152     }
153 
154     if (opts.hasOption<opt::StartServer>() && opts.hasOption<opt::Host>())
155     {
156         std::cout << "Invalid command line arguments. Both --start-server and --connect defined." << std::endl;
157         return false;
158     }
159     else if (!opts.hasOption<opt::StartServer>() && !opts.hasOption<opt::Host>())
160     {
161         std::cout << "Invalid command line arguments. Must define either --start-server or --connect." << std::endl;
162         return false;
163     }
164 
165     if (!opts.hasOption<opt::TestSet>())
166     {
167         std::cout << "Invalid command line arguments. --testset not defined." << std::endl;
168         return false;
169     }
170 
171     if (opts.hasOption<opt::StartServer>())
172     {
173         cmdLine.runMode            = RUNMODE_START_SERVER;
174         cmdLine.serverBinOrAddress = opts.getOption<opt::StartServer>();
175     }
176     else
177     {
178         cmdLine.runMode            = RUNMODE_CONNECT;
179         cmdLine.serverBinOrAddress = opts.getOption<opt::Host>();
180     }
181 
182     if (opts.hasOption<opt::ContinueFile>())
183     {
184         cmdLine.inFile = opts.getOption<opt::ContinueFile>();
185 
186         if (cmdLine.inFile.empty())
187         {
188             std::cout << "Invalid command line arguments. --continue argument is empty." << std::endl;
189             return false;
190         }
191     }
192 
193     cmdLine.port                  = opts.getOption<opt::Port>();
194     cmdLine.caseListDir           = opts.getOption<opt::CaseListDir>();
195     cmdLine.testset               = opts.getOption<opt::TestSet>();
196     cmdLine.exclude               = opts.getOption<opt::ExcludeSet>();
197     cmdLine.outFile               = opts.getOption<opt::TestLogFile>();
198     cmdLine.infoFile              = opts.getOption<opt::InfoLogFile>();
199     cmdLine.summary               = opts.getOption<opt::Summary>();
200     cmdLine.targetCfg.binaryName  = opts.getOption<opt::BinaryName>();
201     cmdLine.targetCfg.workingDir  = opts.getOption<opt::WorkingDir>();
202     cmdLine.targetCfg.cmdLineArgs = opts.getOption<opt::CmdLineArgs>();
203 
204     return true;
205 }
206 
checkCasePathPatternMatch(const char * pattern,const char * casePath,bool isTestGroup)207 bool checkCasePathPatternMatch(const char *pattern, const char *casePath, bool isTestGroup)
208 {
209     int ptrnPos = 0;
210     int casePos = 0;
211 
212     for (;;)
213     {
214         char c = casePath[casePos];
215         char p = pattern[ptrnPos];
216 
217         if (p == '*')
218         {
219             /* Recurse to rest of positions. */
220             int next = casePos;
221             for (;;)
222             {
223                 if (checkCasePathPatternMatch(pattern + ptrnPos + 1, casePath + next, isTestGroup))
224                     return true;
225 
226                 if (casePath[next] == 0)
227                     return false; /* No match found. */
228                 else
229                     next += 1;
230             }
231             DE_ASSERT(false);
232         }
233         else if (c == 0 && p == 0)
234             return true;
235         else if (c == 0)
236         {
237             /* Incomplete match is ok for test groups. */
238             return isTestGroup;
239         }
240         else if (c != p)
241             return false;
242 
243         casePos += 1;
244         ptrnPos += 1;
245     }
246 
247     DE_ASSERT(false);
248     return false;
249 }
250 
readCaseList(xe::TestGroup * root,const char * filename)251 void readCaseList(xe::TestGroup *root, const char *filename)
252 {
253     xe::TestCaseListParser caseListParser;
254     std::ifstream in(filename, std::ios_base::binary);
255     uint8_t buf[1024];
256 
257     XE_CHECK(in.good());
258 
259     caseListParser.init(root);
260 
261     for (;;)
262     {
263         in.read((char *)&buf[0], sizeof(buf));
264         int numRead = (int)in.gcount();
265 
266         if (numRead > 0)
267             caseListParser.parse(&buf[0], numRead);
268 
269         if (numRead < (int)sizeof(buf))
270             break; // EOF
271     }
272 }
273 
readCaseLists(xe::TestRoot & root,const char * caseListDir)274 void readCaseLists(xe::TestRoot &root, const char *caseListDir)
275 {
276     int testCaseListCount = 0;
277     de::DirectoryIterator iter(caseListDir);
278 
279     for (; iter.hasItem(); iter.next())
280     {
281         de::FilePath item = iter.getItem();
282 
283         if (item.getType() == de::FilePath::TYPE_FILE)
284         {
285             string baseName = item.getBaseName();
286             if (baseName.find("-cases.xml") == baseName.length() - 10)
287             {
288                 string packageName     = baseName.substr(0, baseName.length() - 10);
289                 xe::TestGroup *package = root.createGroup(packageName.c_str());
290 
291                 readCaseList(package, item.getPath());
292                 testCaseListCount++;
293             }
294         }
295     }
296 
297     if (testCaseListCount == 0)
298         throw xe::Error("Couldn't find test case lists from test case list directory: '" + string(caseListDir) + "'");
299 }
300 
addMatchingCases(const xe::TestGroup & group,xe::TestSet & testSet,const char * filter)301 void addMatchingCases(const xe::TestGroup &group, xe::TestSet &testSet, const char *filter)
302 {
303     for (int childNdx = 0; childNdx < group.getNumChildren(); childNdx++)
304     {
305         const xe::TestNode *child = group.getChild(childNdx);
306         const bool isGroup        = child->getNodeType() == xe::TESTNODETYPE_GROUP;
307         const string fullPath     = child->getFullPath();
308 
309         if (checkCasePathPatternMatch(filter, fullPath.c_str(), isGroup))
310         {
311             if (isGroup)
312             {
313                 // Recurse into group.
314                 addMatchingCases(static_cast<const xe::TestGroup &>(*child), testSet, filter);
315             }
316             else
317             {
318                 DE_ASSERT(child->getNodeType() == xe::TESTNODETYPE_TEST_CASE);
319                 testSet.add(child);
320             }
321         }
322     }
323 }
324 
removeMatchingCases(const xe::TestGroup & group,xe::TestSet & testSet,const char * filter)325 void removeMatchingCases(const xe::TestGroup &group, xe::TestSet &testSet, const char *filter)
326 {
327     for (int childNdx = 0; childNdx < group.getNumChildren(); childNdx++)
328     {
329         const xe::TestNode *child = group.getChild(childNdx);
330         const bool isGroup        = child->getNodeType() == xe::TESTNODETYPE_GROUP;
331         const string fullPath     = child->getFullPath();
332 
333         if (checkCasePathPatternMatch(filter, fullPath.c_str(), isGroup))
334         {
335             if (isGroup)
336             {
337                 // Recurse into group.
338                 removeMatchingCases(static_cast<const xe::TestGroup &>(*child), testSet, filter);
339             }
340             else
341             {
342                 DE_ASSERT(child->getNodeType() == xe::TESTNODETYPE_TEST_CASE);
343                 testSet.remove(child);
344             }
345         }
346     }
347 }
348 
349 class BatchResultHandler : public xe::TestLogHandler
350 {
351 public:
BatchResultHandler(xe::BatchResult * batchResult)352     BatchResultHandler(xe::BatchResult *batchResult) : m_batchResult(batchResult)
353     {
354     }
355 
setSessionInfo(const xe::SessionInfo & sessionInfo)356     void setSessionInfo(const xe::SessionInfo &sessionInfo)
357     {
358         m_batchResult->getSessionInfo() = sessionInfo;
359     }
360 
startTestCaseResult(const char * casePath)361     xe::TestCaseResultPtr startTestCaseResult(const char *casePath)
362     {
363         // \todo [2012-11-01 pyry] What to do with duplicate results?
364         if (m_batchResult->hasTestCaseResult(casePath))
365             return m_batchResult->getTestCaseResult(casePath);
366         else
367             return m_batchResult->createTestCaseResult(casePath);
368     }
369 
testCaseResultUpdated(const xe::TestCaseResultPtr &)370     void testCaseResultUpdated(const xe::TestCaseResultPtr &)
371     {
372     }
373 
testCaseResultComplete(const xe::TestCaseResultPtr &)374     void testCaseResultComplete(const xe::TestCaseResultPtr &)
375     {
376     }
377 
378 private:
379     xe::BatchResult *m_batchResult;
380 };
381 
readLogFile(xe::BatchResult * batchResult,const char * filename)382 void readLogFile(xe::BatchResult *batchResult, const char *filename)
383 {
384     std::ifstream in(filename, std::ifstream::binary | std::ifstream::in);
385     BatchResultHandler handler(batchResult);
386     xe::TestLogParser parser(&handler);
387     uint8_t buf[1024];
388     int numRead = 0;
389 
390     for (;;)
391     {
392         in.read((char *)&buf[0], DE_LENGTH_OF_ARRAY(buf));
393         numRead = (int)in.gcount();
394 
395         if (numRead <= 0)
396             break;
397 
398         parser.parse(&buf[0], numRead);
399     }
400 
401     in.close();
402 }
403 
printBatchResultSummary(const xe::TestNode * root,const xe::TestSet & testSet,const xe::BatchResult & batchResult)404 void printBatchResultSummary(const xe::TestNode *root, const xe::TestSet &testSet, const xe::BatchResult &batchResult)
405 {
406     int countByStatusCode[xe::TESTSTATUSCODE_LAST];
407     std::fill(&countByStatusCode[0], &countByStatusCode[0] + DE_LENGTH_OF_ARRAY(countByStatusCode), 0);
408 
409     for (xe::ConstTestNodeIterator iter = xe::ConstTestNodeIterator::begin(root);
410          iter != xe::ConstTestNodeIterator::end(root); ++iter)
411     {
412         const xe::TestNode *node = *iter;
413         if (node->getNodeType() == xe::TESTNODETYPE_TEST_CASE && testSet.hasNode(node))
414         {
415             const xe::TestCase *testCase = static_cast<const xe::TestCase *>(node);
416             string fullPath;
417             xe::TestStatusCode statusCode = xe::TESTSTATUSCODE_PENDING;
418             testCase->getFullPath(fullPath);
419 
420             // Parse result data if such exists.
421             if (batchResult.hasTestCaseResult(fullPath.c_str()))
422             {
423                 xe::ConstTestCaseResultPtr resultData = batchResult.getTestCaseResult(fullPath.c_str());
424                 xe::TestCaseResult result;
425                 xe::TestResultParser parser;
426 
427                 xe::parseTestCaseResultFromData(&parser, &result, *resultData.get());
428                 statusCode = result.statusCode;
429             }
430 
431             countByStatusCode[statusCode] += 1;
432         }
433     }
434 
435     printf("\nTest run summary:\n");
436     int totalCases = 0;
437     for (int code = 0; code < xe::TESTSTATUSCODE_LAST; code++)
438     {
439         if (countByStatusCode[code] > 0)
440             printf("  %20s: %5d\n", xe::getTestStatusCodeName((xe::TestStatusCode)code), countByStatusCode[code]);
441 
442         totalCases += countByStatusCode[code];
443     }
444     printf("  %20s: %5d\n", "Total", totalCases);
445 }
446 
writeInfoLog(const xe::InfoLog & log,const char * filename)447 void writeInfoLog(const xe::InfoLog &log, const char *filename)
448 {
449     std::ofstream out(filename, std::ios_base::binary);
450     XE_CHECK(out.good());
451     out.write((const char *)log.getBytes(), log.getSize());
452     out.close();
453 }
454 
createCommLink(const CommandLine & cmdLine)455 xe::CommLink *createCommLink(const CommandLine &cmdLine)
456 {
457     if (cmdLine.runMode == RUNMODE_START_SERVER)
458     {
459         xe::LocalTcpIpLink *link = new xe::LocalTcpIpLink();
460         try
461         {
462             link->start(cmdLine.serverBinOrAddress.c_str(), DE_NULL, cmdLine.port);
463             return link;
464         }
465         catch (...)
466         {
467             delete link;
468             throw;
469         }
470     }
471     else if (cmdLine.runMode == RUNMODE_CONNECT)
472     {
473         de::SocketAddress address;
474 
475         address.setFamily(DE_SOCKETFAMILY_INET4);
476         address.setProtocol(DE_SOCKETPROTOCOL_TCP);
477         address.setHost(cmdLine.serverBinOrAddress.c_str());
478         address.setPort(cmdLine.port);
479 
480         xe::TcpIpLink *link = new xe::TcpIpLink();
481         try
482         {
483             std::string error;
484 
485             link->connect(address);
486             return link;
487         }
488         catch (const std::exception &error)
489         {
490             delete link;
491             throw xe::Error("Failed to connect to ExecServer at: " + cmdLine.serverBinOrAddress + ":" +
492                             de::toString(cmdLine.port) + ", " + error.what());
493         }
494         catch (...)
495         {
496             delete link;
497             throw;
498         }
499     }
500     else
501     {
502         DE_ASSERT(false);
503         return DE_NULL;
504     }
505 }
506 
507 #if (DE_OS == DE_OS_UNIX) || (DE_OS == DE_OS_ANDROID)
508 
509 static xe::BatchExecutor *s_executor = DE_NULL;
510 
signalHandler(int,siginfo_t *,void *)511 void signalHandler(int, siginfo_t *, void *)
512 {
513     if (s_executor)
514         s_executor->cancel();
515 }
516 
setupSignalHandler(xe::BatchExecutor * executor)517 void setupSignalHandler(xe::BatchExecutor *executor)
518 {
519     s_executor = executor;
520     struct sigaction sa;
521 
522     sa.sa_sigaction = signalHandler;
523     sa.sa_flags     = SA_SIGINFO | SA_RESTART;
524     sigfillset(&sa.sa_mask);
525 
526     sigaction(SIGINT, &sa, DE_NULL);
527 }
528 
resetSignalHandler(void)529 void resetSignalHandler(void)
530 {
531     struct sigaction sa;
532 
533     sa.sa_handler = SIG_DFL;
534     sa.sa_flags   = SA_RESTART;
535     sigfillset(&sa.sa_mask);
536 
537     sigaction(SIGINT, &sa, DE_NULL);
538     s_executor = DE_NULL;
539 }
540 
541 #elif (DE_OS == DE_OS_WIN32)
542 
543 static xe::BatchExecutor *s_executor = DE_NULL;
544 
signalHandler(int)545 void signalHandler(int)
546 {
547     if (s_executor)
548         s_executor->cancel();
549 }
550 
setupSignalHandler(xe::BatchExecutor * executor)551 void setupSignalHandler(xe::BatchExecutor *executor)
552 {
553     s_executor = executor;
554     signal(SIGINT, signalHandler);
555 }
556 
resetSignalHandler(void)557 void resetSignalHandler(void)
558 {
559     signal(SIGINT, SIG_DFL);
560     s_executor = DE_NULL;
561 }
562 
563 #else
564 
setupSignalHandler(xe::BatchExecutor *)565 void setupSignalHandler(xe::BatchExecutor *)
566 {
567 }
568 
resetSignalHandler(void)569 void resetSignalHandler(void)
570 {
571 }
572 
573 #endif
574 
runExecutor(const CommandLine & cmdLine)575 void runExecutor(const CommandLine &cmdLine)
576 {
577     xe::TestRoot root;
578 
579     // Read case list definitions.
580     readCaseLists(root, cmdLine.caseListDir.c_str());
581 
582     // Build test set.
583     xe::TestSet testSet;
584 
585     // Build test set.
586     for (vector<string>::const_iterator filterIter = cmdLine.testset.begin(); filterIter != cmdLine.testset.end();
587          ++filterIter)
588         addMatchingCases(root, testSet, filterIter->c_str());
589 
590     if (testSet.empty())
591         throw xe::Error("None of the test case lists contains tests matching any of the test sets.");
592 
593     // Remove excluded cases.
594     for (vector<string>::const_iterator filterIter = cmdLine.exclude.begin(); filterIter != cmdLine.exclude.end();
595          ++filterIter)
596         removeMatchingCases(root, testSet, filterIter->c_str());
597 
598     // Initialize batch result.
599     xe::BatchResult batchResult;
600     xe::InfoLog infoLog;
601 
602     // Read existing results from input file (if supplied).
603     if (!cmdLine.inFile.empty())
604         readLogFile(&batchResult, cmdLine.inFile.c_str());
605 
606     // Initialize commLink.
607     de::UniquePtr<xe::CommLink> commLink(createCommLink(cmdLine));
608 
609     xe::BatchExecutor executor(cmdLine.targetCfg, commLink.get(), &root, testSet, &batchResult, &infoLog);
610 
611     try
612     {
613         setupSignalHandler(&executor);
614         executor.run();
615         resetSignalHandler();
616     }
617     catch (...)
618     {
619         resetSignalHandler();
620 
621         if (!cmdLine.outFile.empty())
622         {
623             xe::writeBatchResultToFile(batchResult, cmdLine.outFile.c_str());
624             printf("Test log written to %s\n", cmdLine.outFile.c_str());
625         }
626 
627         if (!cmdLine.infoFile.empty())
628         {
629             writeInfoLog(infoLog, cmdLine.infoFile.c_str());
630             printf("Info log written to %s\n", cmdLine.infoFile.c_str());
631         }
632 
633         if (cmdLine.summary)
634             printBatchResultSummary(&root, testSet, batchResult);
635 
636         throw;
637     }
638 
639     if (!cmdLine.outFile.empty())
640     {
641         xe::writeBatchResultToFile(batchResult, cmdLine.outFile.c_str());
642         printf("Test log written to %s\n", cmdLine.outFile.c_str());
643     }
644 
645     if (!cmdLine.infoFile.empty())
646     {
647         writeInfoLog(infoLog, cmdLine.infoFile.c_str());
648         printf("Info log written to %s\n", cmdLine.infoFile.c_str());
649     }
650 
651     if (cmdLine.summary)
652         printBatchResultSummary(&root, testSet, batchResult);
653 
654     {
655         string err;
656 
657         if (commLink->getState(err) == xe::COMMLINKSTATE_ERROR)
658             throw xe::Error(err);
659     }
660 }
661 
662 } // namespace
663 
main(int argc,const char * const * argv)664 int main(int argc, const char *const *argv)
665 {
666     CommandLine cmdLine;
667 
668     if (!parseCommandLine(cmdLine, argc, argv))
669         return -1;
670 
671     try
672     {
673         runExecutor(cmdLine);
674     }
675     catch (const std::exception &e)
676     {
677         printf("%s\n", e.what());
678         return -1;
679     }
680 
681     return 0;
682 }
683