text: #!/usr/bin/env python
  
  #
  # NopSCADlib Copyright Chris Palmer 2018
  # nop.head@gmail.com
  # hydraraptor.blogspot.com
  #
  # This file is part of NopSCADlib.
  #
  # NopSCADlib is free software: you can redistribute it and/or modify it under the terms of the
  # GNU General Public License as published by the Free Software Foundation, either version 3 of
  # the License, or (at your option) any later version.
  #
  # NopSCADlib is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
  # without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  # See the GNU General Public License for more details.
  #
  # You should have received a copy of the GNU General Public License along with NopSCADlib.
  # If not, see <https://www.gnu.org/licenses/>.
  #
  #! Runs all the tests in the tests directory and makes the readme file with a catalog of the results.
  
  from __future__ import print_function
  import os
  import sys
  import openscad
  import subprocess
  import bom
  import times
  import options
  import time
  import json
  import shutil
  from deps import *
  from blurb import *
  from colorama import Fore
  
  w = 4096
  h = w
  threshold = 20  # Image comparison allowed number of different pixels
  fuzz = 5       # Image comparison allowed percentage error in pixel value
  
  colour_scheme = "--colorscheme=Nature"
  background = "#F8F8F8"
  
  def do_cmd(cmd, output = sys.stdout):
      for arg in cmd:
          print(arg, end = " ")
      print()
      return subprocess.call(cmd, stdout = output, stderr = output)
  
  def compare_images(a, b, c):
      if not os.path.isfile(a):
          return -1
      log_name = 'magick.log'
      with open(log_name, 'w') as output:
          do_cmd(("magick compare -metric AE -fuzz %d%% %s %s %s" % (fuzz, a, b, c)).split(), output = output)
      with open(log_name, 'r') as f:
          pixels = int(f.read().strip())
      os.remove(log_name)
      return pixels
  
  def update_image(tmp_name, png_name):
      """Update an image only if different, otherwise just change the mod time"""
      diff_name = png_name.replace('.png', '_diff.png')
      pixels = compare_images(png_name, tmp_name, diff_name)
      if pixels < 0 or pixels > threshold:
          shutil.copyfile(tmp_name, png_name)
          print(Fore.YELLOW + png_name + " updated" + Fore.WHITE, pixels if pixels > 0 else '')
      else:
          os.utime(png_name, None)
          os.remove(diff_name)
      os.remove(tmp_name)
  
  
  def depluralise(name):
      if name[-3:] == "ies" and name != "zipties":
          return name[:-3] + 'y'
      if name[-3:] == "hes":
          return name[:-2]
      if name[-1:] == 's':
          return name[:-1]
      return name
  
  def is_plural(name):
      return name != depluralise(name)
  
  def usage():
      print("\nusage:\n\ttests [test_name1] ... [test_nameN] - Run specified tests or all tests in none specified.");
      sys.exit(1)
  
  def tests(tests):
      scad_dir = "tests"
      deps_dir = scad_dir + "/deps"
      png_dir  = scad_dir + "/png"
      bom_dir  = scad_dir + "/bom"
      for dir in [deps_dir, png_dir, bom_dir]:
          if not os.path.isdir(dir):
              os.makedirs(dir)
      doc_name = "readme.md"
      index = {}
      bodies = {}
      done = []
      times.read_times()
      options.check_options(deps_dir)
      #
      # Make cover pic if does not exist as very slow. Delete it to force an update.
      #
      png_name = "libtest.png"
      scad_name = "libtest.scad"
      if not os.path.isfile(png_name):
          openscad.run(colour_scheme, "--projection=p", "--imgsize=%d,%d" % (w, h), "--camera=0,0,0,50,0,340,500", "--autocenter", "--viewall", "-o", png_name, scad_name);
          do_cmd(["magick", png_name, "-trim", "-resize", "1280", "-bordercolor", background, "-border", "10", png_name])
      #
      # List of individual part files
      #
      scads = [i for i in sorted(os.listdir(scad_dir), key = lambda s: s.lower()) if i[-5:] == ".scad"]
  
      for scad in scads:
          base_name = scad[:-5]
          if not tests or base_name in tests:
              done.append(base_name)
              print(base_name)
              cap_name = base_name[0].capitalize() + base_name[1:]
              base_name = base_name.lower()
              scad_name = scad_dir + '/' + scad
              png_name = png_dir + '/' + base_name + '.png'
              bom_name = bom_dir + '/' + base_name + '.json'
  
              objects_name = None
              vits_name = 'vitamins/' + base_name + '.scad'
              if is_plural(base_name) and os.path.isfile(vits_name):
                  objects_name = vits_name
  
              locations = [
                  ('vitamins/' + depluralise(base_name) + '.scad', 'Vitamins'),
                  ('printed/' + base_name + '.scad',               'Printed'),
                  ('utils/' + base_name + '.scad',                 'Utilities'),
                  ('utils/core/' + base_name + '.scad',            'Core Utilities'),
              ]
  
              for name, type in locations:
                  if os.path.isfile(name):
                      impl_name = name
                      break
              else:
                  print("Can't find implementation!")
                  continue
  
              vsplit = "AJR" + chr(ord('Z') + 1)
              vtype = locations[0][1]
              types = [vtype + ' ' + vsplit[i] + '-'  + chr(ord(vsplit[i + 1]) - 1) for i in range(len(vsplit) - 1)] + [loc[1] for loc in locations[1 :]]
              if type == vtype:
                  for i in range(1, len(vsplit)):
                      if cap_name[0] < vsplit[i]:
                           type = types[i - 1]
                           break
  
              for t in types:
                  if not t in bodies:
                      bodies[t] = []
                      index[t] = []
  
              body = bodies[type]
  
              index[type] += [cap_name]
              body += ['<a name="%s"></a>' % cap_name]
              body += ["## " + cap_name]
  
              doc = None
              if impl_name:
                  doc = scrape_code(impl_name)
                  blurb = doc["blurb"]
              else:
                  blurb = scrape_blurb(scad_name)
  
              if not len(blurb):
                  print("Blurb not found!")
              else:
                  body += [ blurb ]
  
              if objects_name:
                  body += ["[%s](%s) Object definitions.\n" % (objects_name, objects_name)]
  
              if impl_name:
                  body += ["[%s](%s) Implementation.\n" % (impl_name, impl_name)]
  
              body += ["[%s](%s) Code for this example.\n" % (scad_name.replace('\\','/'), scad_name)]
  
              if doc:
                  for thing, heading in [("properties", "Function"), ("functions", "Function"), ("modules", "Module")]:
                      things = doc[thing]
                      if things:
                          body += ['### %s\n| %s | Description |\n|:--- |:--- |' % (thing.title(), heading)]
                          for item in sorted(things):
                              body += ['| ```%s``` | %s |' % (item, things[item])]
                          body += ['']
  
              body += ["![%s](%s)\n" %(base_name, png_name)]
  
              dname = deps_name(deps_dir, scad.lower())
              oldest = png_name if mtime(png_name) < mtime(bom_name) else bom_name
              changed = check_deps(oldest, dname)
              changed = times.check_have_time(changed, scad_name)
              changed = options.have_changed(changed, oldest)
              if changed:
                  print(changed)
                  t = time.time()
                  tmp_name = 'tmp.png'
                  openscad.run_list(options.list() + ["-D$bom=2", colour_scheme, "--projection=p", "--imgsize=%d,%d" % (w, h), "--camera=0,0,0,70,0,315,500", "--autocenter", "--viewall", "-d", dname, "-o", tmp_name, scad_name]);
                  times.add_time(scad_name, t)
                  do_cmd(["magick", tmp_name, "-trim", "-resize", "1000x600", "-bordercolor", background, "-border", "10", tmp_name])
                  update_image(tmp_name, png_name)
                  BOM = bom.parse_bom()
                  with open(bom_name, 'wt') as outfile:
                      json.dump(BOM.flat_data(), outfile, indent = 4)
  
              with open(bom_name, "rt") as bom_file:
                  BOM = json.load(bom_file)
                  for thing, heading in [("vitamins", "Module call | BOM entry") , ("printed", "Filename"), ("routed", "Filename"), ("assemblies", "Name")]:
                      things = BOM[thing]
                      if things:
                          body += ['### %s\n| Qty | %s |\n| ---:|:--- |%s' % (thing.title(), heading, ':---|' if '|' in heading else '')]
                          for item in sorted(things, key = lambda s: s.split(":")[-1]):
                              name = item
                              desc = ''
                              if thing == "vitamins":
                                  vit = item.split(':')
                                  name = '```' + vit[0] + '```' if vit[0] else ''
                                  while '[[' in name and ']]' in name:
                                      i = name.find('[[')
                                      j = name.find(']]') + 2
                                      name = name.replace(name[i : j], '[ ... ]')
                                  desc = vit[1]
                                  body += ['| %3d | %s | %s |' % (things[item]["count"], name, desc)]
                              else:
                                  count = things[item] if thing == 'assemblies' else things[item]["count"]
                                  body += ['| %3d | %s |' % (count, name)]
                          body += ['']
  
              body += ['\n<a href="#top">Top</a>']
              body += ["\n---"]
  
      for test in done:
          if test in tests:
              tests.remove(test)
      if tests:
          for test in tests:
              print(Fore.MAGENTA + "Could not find a test called", test, Fore.WHITE)
          usage()
  
      with open(doc_name, "wt") as doc_file:
          print('# NopSCADlib', file = doc_file)
          print('''\
  An ever expanding library of parts modelled in OpenSCAD useful for 3D printers and enclosures for electronics, etc.
  
  It contains lots of vitamins (the RepRap term for non-printed parts), some general purpose printed parts and
  some utilities. There are also Python scripts to generate Bills of Materials (BOMs),
   STL files for all the printed parts, DXF files for CNC routed parts in a project and a manual containing assembly
  instructions and exploded views by scraping markdown embedded in OpenSCAD comments, [see scripts](scripts/readme.md). A simple example project can be found [here](examples/MainsBreakOutBox/readme.md).
  
  For more examples of what it can make see the [gallery](gallery/readme.md).
  
  The license is GNU General Public License v3.0, see [COPYING](COPYING).
  
  See [usage](docs/usage.md) for requirements, installation instructions and a usage guide.
  
  <img src="libtest.png" width="100%"/>\n
  ''', file = doc_file)
  
          print('## Table of Contents<a name="top"/>', file = doc_file)
          print('<table><tr>', file = doc_file)
          n = 0
          for type in types:
              print('<th align="left"> %s </th>' % type, end = '', file = doc_file)
              n = max(n, len(index[type]))
          print('</tr>',  file = doc_file)
          for i in range(n):
              print('<tr>',  file = doc_file, end = '')
              for type in types:
                  if i < len(index[type]):
                      name = sorted(index[type])[i]
                      print('<td> <a href = "#' + name + '">' + name + '</a> </td>', file = doc_file, end = '')
                  else:
                      print('<td></td>', file = doc_file, end = '')
              print('</tr>', file = doc_file)
          print('</table>\n\n---', file = doc_file)
          for type in types:
              for line in bodies[type]:
                  print(line, file = doc_file)
      with open("readme.html", "wt") as html_file:
          do_cmd("python -m markdown -x tables readme.md".split(), html_file)
      times.print_times()
      do_cmd('codespell -L od readme.md'.split())
  
  if __name__ == '__main__':
      for arg in sys.argv[1:]:
          if arg[:1] == '-': usage()
      tests(sys.argv[1:])
  