test.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670
  1. import os
  2. import argparse
  3. import csv
  4. import sys
  5. import datetime
  6. import shutil
  7. import subprocess
  8. from config import BASE_PATH, SOURCE_PATH, DL_PATH, DL_LOG
  9. import rewise
  10. from dl import downloadFile
  11. from sources import InstallerSourceUniqIt, InstallerSourceAllUniqIt
  12. from installers import InstallerStats, installerStatsCsvIt, acceptableInstallerIt
  13. KiB = 1024
  14. MiB = KiB ** 2
  15. GiB = KiB ** 3
  16. TiB = KiB ** 4
  17. def formatFileSize(fileSize):
  18. """
  19. @param fileSize: File size in bytes to format to string.
  20. @type fileSize: uint
  21. """
  22. sizeStr = ""
  23. if fileSize > TiB: # TiB
  24. sizeStr = f"{fileSize / TiB:.2f} TB"
  25. elif fileSize > GiB: # GiB
  26. sizeStr = f"{fileSize / GiB:.2f} GB"
  27. elif fileSize > MiB: # MiB
  28. sizeStr = f"{fileSize / MiB:.2f} MB"
  29. elif fileSize > KiB: # KiB
  30. sizeStr = f"{fileSize / KiB:.2f} KB"
  31. else:
  32. sizeStr = f"{fileSize} Bytes"
  33. return sizeStr
  34. def getMd5Sum(file):
  35. try:
  36. output = subprocess.check_output(["md5sum", file])
  37. except subprocess.CalledProcessError as err:
  38. return ""
  39. else:
  40. return output.decode('utf-8').split(" ")[0]
  41. def getB3Sum(file):
  42. try:
  43. output = subprocess.check_output(["b3sum", file])
  44. except subprocess.CalledProcessError as err:
  45. return ""
  46. else:
  47. return output.decode('utf-8').split(" ")[0]
  48. ESC = "\x1B"
  49. CSI = "\x9B"
  50. def clearLine(file=sys.stdout):
  51. print(f"{ESC}[0G", end="", file=file) # go to line begin
  52. print(f"{ESC}[0K", end="", file=file) # clear line
  53. def saveDownloadLog(logs):
  54. if not logs:
  55. return
  56. with open(DL_LOG, "w+", newline="") as fp:
  57. writer = csv.writer(fp, dialect="unix", quoting=csv.QUOTE_STRINGS)
  58. for row in logs:
  59. writer.writerow(row)
  60. def addErrorLog(logs, errorType, filepath, message=""):
  61. logs.append([datetime.datetime.now(), errorType, filepath, message])
  62. DOWNLOAD_ARGS = [
  63. (["--simulate"], {"dest":"simulate", "action":"store_true", "help":"Don't download files, just check missing and corrupt."}),
  64. (["--skip-sum"], {"dest":"skip_sum", "action":"store_true", "help":"Skip Blake 3 sum check for already present files."}),
  65. (None, {"dest":"IN_SOURCE", "type":str, "help":"The installer source .csv"})
  66. ]
  67. def download(namespace):
  68. sourceFile = namespace.IN_SOURCE
  69. simulate = namespace.simulate
  70. skipsum = namespace.skip_sum
  71. sources = list(InstallerSourceUniqIt(sourceFile))
  72. total = len(sources)
  73. totalSize = 0
  74. toDlSize = 0
  75. current = 1
  76. errors = 0
  77. log = []
  78. # Get total sizes
  79. for source in sources:
  80. if os.path.isfile(source.filepath):
  81. if os.path.getsize(source.filepath) != source.filesize:
  82. toDlSize += source.filesize
  83. else:
  84. toDlSize += source.filesize
  85. totalSize += source.filesize
  86. # Check free disc space
  87. if shutil.disk_usage(DL_PATH).free < toDlSize:
  88. print(f"Not enough free space at {DL_PATH}; at least {formatFileSize(toDlSize)} is needed.")
  89. return
  90. for source in sources:
  91. clearLine()
  92. print(f"[{current:4d} / {total:4d}] [Errors: {errors}] {source.b3} {source.getFilename()}", end="", flush=True)
  93. localfile = source.filepath
  94. localpath = os.path.dirname(localfile)
  95. needDl = False
  96. if os.path.isfile(localfile):
  97. if os.path.getsize(localfile) != source.filesize:
  98. addErrorLog(log, "LOCAL_SIZE", source.filepath)
  99. errors += 1
  100. current += 1
  101. continue
  102. if not skipsum:
  103. b3 = getB3Sum(localfile)
  104. if b3 == source.b3:
  105. current += 1
  106. continue
  107. else:
  108. addErrorLog(log, "LOCAL_SUM", source.filepath)
  109. errors += 1
  110. current += 1
  111. continue
  112. else:
  113. current += 1
  114. continue
  115. else:
  116. needDl = True
  117. if simulate:
  118. current += 1
  119. continue
  120. dlerror = downloadFile(source.dlUrl, localfile)
  121. if dlerror:
  122. # errors should have been printed..
  123. if os.path.exists(localfile):
  124. os.remove(localfile)
  125. addErrorLog(log, "DL", source.filepath, dlerror)
  126. continue
  127. b3 = getB3Sum(localfile)
  128. if b3 != source.b3:
  129. addErrorLog(log, "DL_SUM", source.filepath)
  130. continue
  131. current += 1
  132. print()
  133. saveDownloadLog(log)
  134. print("Total size:", formatFileSize(totalSize))
  135. INIT_ARGS = [
  136. (None, {"dest":"IN_SOURCE", "type":str, "help":"The installer source .csv"}),
  137. (None, {"dest":"OUT_RESULT", "type":str, "help":"The output result .csv"})
  138. ]
  139. def init(namespace):
  140. sourceFile = namespace.IN_SOURCE
  141. resultFile = namespace.OUT_RESULT
  142. with open(resultFile, "w", newline="") as fp:
  143. writer = csv.writer(fp, dialect="unix")
  144. # Write the header
  145. writer.writerow(InstallerStats.columns())
  146. sources = list(InstallerSourceUniqIt(sourceFile))
  147. total = len(sources)
  148. current = 1
  149. errors = 0
  150. for source in sources:
  151. installer = InstallerStats(source)
  152. clearLine()
  153. print(f"[{current:4d} / {total:4d}] [Errors: {errors:4d}] {installer.filename}", end="", flush=True)
  154. installer.fullTest()
  155. # analyze errors
  156. if installer.devStatus != "OK":
  157. installer.generateComments()
  158. errors += 1
  159. writer.writerow(installer.asRow())
  160. current += 1
  161. print()
  162. UPDATE_ARGS = [
  163. (None, {"dest":"IN_SOURCE", "type":str, "help":"The installer source .csv"}),
  164. (None, {"dest":"OUT_RESULT", "type":str, "help":"The output result .csv"}),
  165. (["--only-new"], {"dest":"only_new", "action":"store_true", "help":"Only run tests on new found installers"})
  166. ]
  167. def update(namespace):
  168. sourceFile = namespace.IN_SOURCE
  169. resultFile = namespace.OUT_RESULT
  170. only_new = namespace.only_new
  171. new = [installer for installer in installerStatsCsvIt(resultFile)]
  172. oldmd5 = [installer.md5 for installer in new] # new is still old here :')
  173. for source in InstallerSourceUniqIt(sourceFile):
  174. if source.md5 in oldmd5:
  175. continue
  176. new.append(InstallerStats(source))
  177. new.sort()
  178. with open(resultFile, "w", newline="") as fp:
  179. writer = csv.writer(fp, dialect="unix")
  180. # Write the header
  181. writer.writerow(InstallerStats.columns())
  182. total = len(new)
  183. current = 1
  184. for installer in new:
  185. if only_new:
  186. if installer.md5 not in oldmd5:
  187. print(f"[{current:4d} / {total:4d}] NEW: {installer.filepath}")
  188. installer.fullTest()
  189. installer.generateComments()
  190. else:
  191. clearLine()
  192. print(f"[{current:4d} / {total:4d}] {installer.filepath}", end="", flush=True)
  193. installer.testDevVerify()
  194. installer.testDevList()
  195. installer.testDevRaw()
  196. if installer.md5 not in oldmd5:
  197. installer.fullTest()
  198. installer.generateComments()
  199. writer.writerow(installer.asRow())
  200. current += 1
  201. print()
  202. CREATE_DIFF_ARGS = [
  203. (None, {"dest":"IN_RESULT", "type":str, "help":"The input result .csv"}),
  204. (None, {"dest":"OUT_RESULT", "type":str, "help":"The output result .csv"})
  205. ]
  206. def create_diff(namespace):
  207. oldResultFile = namespace.IN_RESULT
  208. resultFile = namespace.OUT_RESULT
  209. old = [installer for installer in installerStatsCsvIt(oldResultFile)]
  210. with open(resultFile, "w", newline="") as fp:
  211. writer = csv.writer(fp, dialect="unix")
  212. # Write the header
  213. writer.writerow(InstallerStats.columns())
  214. total = len(old)
  215. current = 1
  216. errors = 0
  217. for installer in old:
  218. clearLine()
  219. print(f"[{current:4d} / {total:4d}] [Err {errors:4d}] {installer.filepath}", end="", flush=True)
  220. installer.testDevVerify()
  221. installer.testDevList()
  222. installer.testDevRaw()
  223. if installer.devStatus != "OK":
  224. installer.generateComments()
  225. errors += 1
  226. writer.writerow(installer.asRow())
  227. current += 1
  228. print()
  229. FILTER_INVALID_ARGS = [
  230. (None, {"dest":"IN_RESULT", "type":str, "help":"The input result .csv"}),
  231. (None, {"dest":"OUT_RESULT", "type":str, "help":"The output result .csv"})
  232. ]
  233. def filter_invalid(namespace):
  234. resultFile = namespace.IN_RESULT
  235. newResultFile = namespace.OUT_RESULT
  236. with open(newResultFile, "w", newline="") as fp:
  237. writer = csv.writer(fp, dialect="unix")
  238. # Write the header
  239. writer.writerow(InstallerStats.columns())
  240. for installer in acceptableInstallerIt(resultFile):
  241. writer.writerow(installer.asRow())
  242. PRINT_DIFF_ARGS = [
  243. (None, {"dest":"IN_RESULT1", "type":str, "help":"Old result to compare"}),
  244. (None, {"dest":"IN_RESULT2", "type":str, "help":"New result to compare"})
  245. ]
  246. def print_diff(namespace):
  247. resultFileOld = namespace.IN_RESULT1
  248. resultFileNew = namespace.IN_RESULT2
  249. # - new
  250. # - fixed verify
  251. # - broke verify
  252. old = [installer for installer in installerStatsCsvIt(resultFileOld)]
  253. totalDevVerifyOk = 0
  254. totalDevVerifyError = 0
  255. verifyFixed = []
  256. verifyBroke = []
  257. for newInst in installerStatsCsvIt(resultFileNew):
  258. for oldInst in old:
  259. if newInst.md5 != oldInst.md5:
  260. continue
  261. if newInst.devStatus != oldInst.devStatus:
  262. if oldInst.devStatus == "OK":
  263. verifyBroke.append((oldInst.b3, oldInst.peBuild, oldInst.filename))
  264. elif newInst.devStatus == "OK":
  265. verifyFixed.append((oldInst.b3, oldInst.peBuild, oldInst.filename))
  266. if newInst.devStatus == "OK":
  267. totalDevVerifyOk += 1
  268. else:
  269. totalDevVerifyError += 1
  270. break
  271. print(f"Fixed ({len(verifyFixed)})")
  272. print("--------------------------------")
  273. for fixed in verifyFixed:
  274. print(fixed[0], fixed[2], end="\n")
  275. print()
  276. print(f"Broke ({len(verifyBroke)})")
  277. print("--------------------------------")
  278. for broke in verifyBroke:
  279. print(broke[0], broke[2], end="\n")
  280. PRINT_STATS_ARGS = [
  281. (None, {"dest":"IN_RESULT", "type":str, "help":"Result file"})
  282. ]
  283. def print_stats(namespace):
  284. resultFile = namespace.IN_RESULT
  285. BUILDDATE_VERSION_MAP = {
  286. "1998-11-09 21:17:09": "InstallMaster 7",
  287. "2000-04-25 16:37:12": "InstallMaster 8",
  288. "2001-10-25 21:47:11": "Installation System 9"
  289. }
  290. BuildStats = {}
  291. TotalOk = 0
  292. TotalErr = 0
  293. ListTotalOk = 0
  294. ListTotalErr = 0
  295. RawTotalOk = 0
  296. RawTotalErr = 0
  297. R02TotalOk = 0
  298. R02TotalErr = 0
  299. pkOk = 0
  300. pkError = 0
  301. pkRawOk = 0
  302. pkRawError = 0
  303. # Find installers that work on v0.2.0 but not on dev
  304. workingOn02NotOnDev = []
  305. errorWoComment = 0
  306. installers = list(installerStatsCsvIt(resultFile))
  307. for installer in installers:
  308. if installer.peBuild not in BuildStats:
  309. BuildStats.update({
  310. installer.peBuild: {
  311. "error": 0,
  312. "ok": 0,
  313. "listError": 0,
  314. "listOk": 0,
  315. "rawError": 0,
  316. "rawOk": 0,
  317. "r02Ok": 0,
  318. "r02Error": 0
  319. }
  320. })
  321. if installer.devStatus != "OK":
  322. BuildStats[installer.peBuild]["error"] += 1
  323. else:
  324. BuildStats[installer.peBuild]["ok"] += 1
  325. if installer.devRawStatus != "OK":
  326. BuildStats[installer.peBuild]["rawError"] += 1
  327. else:
  328. BuildStats[installer.peBuild]["rawOk"] += 1
  329. if installer.devListStatus != "OK":
  330. BuildStats[installer.peBuild]["listError"] += 1
  331. else:
  332. BuildStats[installer.peBuild]["listOk"] += 1
  333. if installer.rew02Status != "OK":
  334. BuildStats[installer.peBuild]["r02Error"] += 1
  335. else:
  336. BuildStats[installer.peBuild]["r02Ok"] += 1
  337. # More stats
  338. CommentStats = {}
  339. VerifyErrorStats = {}
  340. for installer in installers:
  341. if installer.devStatus != "OK":
  342. TotalErr += 1
  343. if not installer.comment:
  344. errorWoComment += 1
  345. if installer.isPk:
  346. pkError += 1
  347. comments = installer.comment.split("\n---\n")
  348. comment = comments[-1]
  349. if comment:
  350. if comment not in CommentStats:
  351. CommentStats.update({comment:0})
  352. CommentStats[comment] += 1
  353. error = installer.devError.rstrip().split("\n")[-1]
  354. # strip unique stuff
  355. if "ERROR: Failed seek to file offset" in error:
  356. error = "ERROR: Failed seek to file offset"
  357. elif "ERROR: parseWiseScript unknown OP" in error:
  358. error = error[:37]
  359. error = f"{installer.devStatus.ljust(18)} {error}"
  360. if error not in VerifyErrorStats:
  361. VerifyErrorStats.update({error: 0})
  362. VerifyErrorStats[error] += 1
  363. else:
  364. TotalOk += 1
  365. if installer.isPk:
  366. pkOk += 1
  367. if installer.devRawStatus != "OK":
  368. RawTotalErr += 1
  369. if installer.isPk:
  370. pkRawError += 1
  371. else:
  372. RawTotalOk += 1
  373. if installer.isPk:
  374. pkRawOk += 1
  375. if installer.devListStatus != "OK":
  376. ListTotalErr += 1
  377. else:
  378. ListTotalOk += 1
  379. if installer.rew02Status != "OK":
  380. R02TotalErr += 1
  381. else:
  382. R02TotalOk += 1
  383. if installer.devStatus != "OK":
  384. workingOn02NotOnDev.append(installer.filepath)
  385. # Print stuff
  386. print("| DATE | OK | ERR | LOK | LERR | ROK | RERR | v02 | v02E | VERSION")
  387. print("| :------------------ | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | :------")
  388. for build in sorted(list(BuildStats.keys())):
  389. stats = BuildStats[build]
  390. d = datetime.datetime.fromtimestamp(int(build)).strftime('%Y-%m-%d %H:%M:%S')
  391. if d == "1970-01-01 01:00:00":
  392. print("| NE ", end="")
  393. else:
  394. #d = build
  395. print(f"| {d}", end="")
  396. print(f" | {stats['ok']:4d}", end="")
  397. print(f" | {stats['error']:4d}", end="")
  398. print(f" | {stats['listOk']:4d}", end="")
  399. print(f" | {stats['listError']:4d}", end="")
  400. print(f" | {stats['rawOk']:4d}", end="")
  401. print(f" | {stats['rawError']:4d}", end="")
  402. print(f" | {stats['r02Ok']:4d}", end="")
  403. print(f" | {stats['r02Error']:4d}", end="")
  404. print(" | ", end="")
  405. if d in BUILDDATE_VERSION_MAP:
  406. print(BUILDDATE_VERSION_MAP[d])
  407. else:
  408. print(f"Unknown")
  409. print("| ------------------- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | -------")
  410. print(f"| Total: {TotalOk + TotalErr:12d}", end="")
  411. print(f" | {TotalOk:4d}", end="")
  412. print(f" | {TotalErr:4d}", end="")
  413. print(f" | {ListTotalOk:4d}", end="")
  414. print(f" | {ListTotalErr:4d}", end="")
  415. print(f" | {RawTotalOk:4d}", end="")
  416. print(f" | {RawTotalErr:4d}", end="")
  417. print(f" | {R02TotalOk:4d}", end="")
  418. print(f" | {R02TotalErr:4d}", end="")
  419. print(" | ")
  420. print()
  421. print("PK Total :", pkOk + pkError)
  422. print("PK OK :", pkOk)
  423. print("PK ERROR :", pkError)
  424. print("PK Raw OK :", pkRawOk)
  425. print("PK Raw ERROR:", pkRawError)
  426. print()
  427. print("Comments:")
  428. print(f"{errorWoComment:3d} Without comment")
  429. for comment, count in sorted(CommentStats.items(), key=lambda x: x[1], reverse=True):
  430. print(f"{count:3d} {comment}")
  431. print()
  432. print("Verify errors:")
  433. for error, count in sorted(VerifyErrorStats.items(), key=lambda x: x[1], reverse=True):
  434. print(f"{count:3d} {error}")
  435. if workingOn02NotOnDev:
  436. print()
  437. print("These installers work on v0.2.0 but not on dev:")
  438. for filepath in workingOn02NotOnDev:
  439. print(" -", filepath)
  440. UPDATE_COMMENTS_ARGS = [
  441. (None, {"dest":"IN_OUT_RESULT", "type":str, "help":"Result file"}),
  442. ]
  443. def update_comments(namespace):
  444. resultFile = namespace.IN_OUT_RESULT
  445. installers = [installer for installer in installerStatsCsvIt(resultFile)]
  446. with open(resultFile, "w", newline="") as fp:
  447. writer = csv.writer(fp, dialect="unix")
  448. # Write the header
  449. writer.writerow(InstallerStats.columns())
  450. for installer in installers:
  451. if installer.devStatus != "OK":
  452. installer.clearComments()
  453. installer.gatherInfo()
  454. installer.generateComments()
  455. writer.writerow(installer.asRow())
  456. RESULT_TO_SOURCE_ARGS = [
  457. (None, {"dest":"IN_RESULT", "type":str, "help":"Result file"}),
  458. (None, {"dest":"OUT_SOURCE", "type":str, "help":"New source file"}),
  459. ]
  460. def result_to_source(namespace):
  461. resultFile = namespace.IN_RESULT
  462. sourceFile = namespace.OUT_SOURCE
  463. with open(sourceFile, "w", newline="") as fp:
  464. writer = csv.writer(fp, dialect="unix")
  465. for installer in installerStatsCsvIt(resultFile):
  466. writer.writerow([installer.md5, installer.filepath, installer.dlUrl])
  467. def add_parser(subparsers, operation, args, func, help=""):
  468. parser = subparsers.add_parser(operation, help=help)
  469. for arg in args:
  470. args, kwargs = arg
  471. if args is None:
  472. parser.add_argument(**kwargs)
  473. else:
  474. parser.add_argument(*args, **kwargs)
  475. parser.set_defaults(func=func)
  476. return parser
  477. def indent(s, spaces):
  478. newstr = ""
  479. istr = ''.ljust(spaces)
  480. for line in s.split("\n")[2:-1]: # TODO [2:]
  481. newstr += f"{istr}{line}\n"
  482. return newstr
  483. def print_full_help(parser, subparsers):
  484. parser.print_help(sys.stderr)
  485. print()
  486. print("---------\n")
  487. for p in parsers:
  488. print(f"{p.prog.split(' ')[-1]}:")
  489. print(indent(p.format_help(), 2))
  490. if __name__ == "__main__":
  491. parser = argparse.ArgumentParser()
  492. subparsers = parser.add_subparsers(title="COMMANDS",
  493. description=("Note: use -h after a command to get more help on that "
  494. "command"))
  495. parsers = [
  496. add_parser(subparsers, "download", DOWNLOAD_ARGS, download,
  497. help=("Download installers from the given SOURCE file "
  498. "when needed")),
  499. add_parser(subparsers, "init", INIT_ARGS, init,
  500. help=("Run initial test, the output "
  501. "will be written to the given RESULT file. The "
  502. "initial test does include testing against REWise "
  503. "v0.2")),
  504. add_parser(subparsers, "update", UPDATE_ARGS, update,
  505. help=("Add new installers from given SOURCE and re-run "
  506. "tests for all installers, excluding the REWise "
  507. "v0.2 tests. To only add new and run tests for "
  508. "new installers give the '--only-new' argument.")),
  509. add_parser(subparsers, "create-diff", CREATE_DIFF_ARGS, create_diff,
  510. help=("Re-run tests of first given RESULT, ouput to "
  511. "second given RESULT file.")),
  512. add_parser(subparsers, "filter-invalid", FILTER_INVALID_ARGS, filter_invalid,
  513. help=("Filter out results that appear to be invalid "
  514. "files / not Wise installers.")),
  515. add_parser(subparsers, "update-comments", UPDATE_COMMENTS_ARGS, update_comments,
  516. help="Update comments for given RESULT file."),
  517. add_parser(subparsers, "print-diff", PRINT_DIFF_ARGS, print_diff,
  518. help=("Compare two RESULT files and print the installers "
  519. "that it fixed/broke.")),
  520. add_parser(subparsers, "print-stats", PRINT_STATS_ARGS, print_stats,
  521. help="Print some stats for the given RESULT file."),
  522. add_parser(subparsers, "result-to-source", RESULT_TO_SOURCE_ARGS,
  523. result_to_source,
  524. help="Convert RESULT file into SOURCE file.")
  525. ]
  526. args = parser.parse_args()
  527. if "func" not in args:
  528. parser.print_help(sys.stderr)
  529. #print_full_help(parser, parsers)
  530. sys.exit(1)
  531. args.func(args)