213 """The engine comparison: local tooling for size and complexity, Sonar for cognitive.
215 Comparing the projects as a whole compares their feature sets: the set of pixel
216 operations under src/iop has diverged between the forks, and those modules are
217 independent of one another, so their bulk says little about maintainability.
218 Subtracting them compares the engine, which is what both projects need whatever
221 Cyclomatic complexity, lines of code and comment ratio come from ONE tool applied
222 identically to every version, because SonarCloud and lizard do not define
223 cyclomatic complexity the same way and mixing them silently compares nothing.
224 Cognitive complexity has no local equivalent, so it is reported from SonarCloud for
225 the three versions that have a project there, and left blank for the rest rather
233 columns, sonar = [], {}
234 for item
in spec.split(
","):
236 if not item
or item.startswith(
"exclude="):
238 key, _, label = item.partition(
"=")
239 label = label.strip()
or key.strip()
240 columns.append(label)
242 sonar[label] = key
if key
and key !=
"-" else None
246 raise RuntimeError(
"local engine measurement unavailable")
249 for label
in columns:
250 if label
in FROZEN_ENGINE:
251 data[label] = dict(FROZEN_ENGINE[label])
253 data[label] = dict(local)
254 key = sonar.get(label)
266 m = [
"cognitive_complexity"]
267 total =
fetch(key, m)
268 cog = int(float(total.get(
"cognitive_complexity", 0)))
269 for sub
in (
"src/iop",
"src/external"):
271 part =
fetch(
"%s:%s" % (key, sub), m)
272 cog -= int(float(part.get(
"cognitive_complexity", 0)))
277 data[label][
"cognitive"] = cog
280 return "%.1f %%" % (100.0 * d[
"comment"] /
max(1, d[
"comment"] + d[
"code"]))
284 kept_docs, kept_other = {}, {}
285 for line
in (previous
or "").splitlines():
286 for prefix, store
in ((
"| Functions carrying documentation", kept_docs),
287 (
"| Types, constants and macros carrying documentation",
289 if line.startswith(prefix):
290 cells = [c.strip()
for c
in line.strip().strip(
"|").split(
"|")]
291 for label, value
in zip(columns, cells[1:]):
292 if value
and value !=
"—":
295 if live_docs
is None:
299 sys.stderr.write(
"readme-metrics: built a Doxygen symbol table for "
300 "documentation coverage\n")
302 for label
in columns:
304 if label
in FROZEN_DOCS:
305 f = FROZEN_DOCS[label]
306 d[
"docs"] =
"%.1f %%" % (100.0 * f[
"documented"] / f[
"functions"])
308 d[
"docs"] =
"%.1f %%" % (100.0 * live_docs[
"documented"] / live_docs[
"functions"])
310 d[
"docs"] = kept_docs.get(label,
"—")
311 if label
in FROZEN_DOCS_OTHER:
312 f = FROZEN_DOCS_OTHER[label]
313 d[
"docs_other"] =
"%.1f %%" % (100.0 * f[
"documented"] / f[
"symbols"])
314 elif live_docs
and live_docs.get(
"other_symbols"):
315 d[
"docs_other"] =
"%.1f %%" % (100.0 * live_docs[
"other_documented"]
316 / live_docs[
"other_symbols"])
318 d[
"docs_other"] = kept_other.get(label,
"—")
320 rows = [(
"Cyclomatic complexity",
lambda d:
"{:,}".format(d[
"complexity"])),
321 (
"Lines of code",
lambda d:
"{:,}".format(d[
"code"])),
322 (
"Comment lines",
lambda d:
"{:,}".format(d[
"comment"])),
323 (
"Ratio of comments", ratio),
324 (
"Cognitive complexity",
325 lambda d:
"{:,}".format(d[
"cognitive"])
if d[
"cognitive"]
else "—"),
326 (
"Functions carrying documentation",
lambda d: d[
"docs"]),
327 (
"Types, constants and macros carrying documentation",
328 lambda d: d[
"docs_other"])]
329 out = [
"| Metric | " +
" | ".join(columns) +
" |",
330 "| ------ | " +
" | ".join(
"-----------:" for _
in columns) +
" |"]
331 for label, render
in rows:
332 out.append(
"| " + label +
" | " +
" | ".join(render(data[c])
for c
in columns) +
" |")
333 return "\n".join(out) +
"\n"
444 """Include-graph exposure of this tree's engine."""
446 ch.EXCLUDED_DIR_PARTS[:] = [p
for p
in ENGINE_EXCLUDE]
447 ch.load_submodule_exclusions(repo_root)
451 edges = ch.source_include_edges(
".", source_dir)
or []
457 succ, pred, nodes = {}, {}, set()
459 succ.setdefault(a, set()).add(b)
460 pred.setdefault(b, set()).add(a)
463 def closure(adj, start):
464 seen, stack = set(), [start]
467 for v
in adj.get(u, ()):
475 headers = [f
for f
in nodes
if f.lower().endswith((
".h",
".hpp",
".hxx"))]
476 sources = [f
for f
in nodes
if f
not in headers]
477 dep = sorted(len(closure(succ, f)) / n * 100
for f
in sources)
478 aff = [len(closure(pred, h))
for h
in headers]
479 cycles = [c
for c
in ch.strongly_connected(sorted(nodes), succ)
if len(c) > 1]
480 god = len({a
for a, b
in edges
481 if b.endswith(
"darktable.h")
and a.lower().endswith((
".h",
".hpp"))})
482 return {
"med_dep": round(dep[len(dep) // 2], 1),
483 "avg_aff": int(round(sum(aff) /
max(1, len(aff)))),
484 "over25": int(round(100.0 * sum(1
for a
in aff
if a / n > 0.25) / len(headers))),
485 "cycles": len(cycles),
486 "trapped": sum(len(c)
for c
in cycles),
612 """Upper-triangle similarity matrix.
614 Release-to-release cells are frozen. The cells involving Ansel move with Ansel and
615 need the Darktable sources to recompute, so they are refreshed only when
616 --darktable-trees points at a directory holding dt38/ dt40/ dt50/ dt56/ checkouts.
617 Without it the values already in the README are KEPT, not blanked: a table that
618 loses real numbers because an optional input was missing is worse than one that is
619 slightly out of date, and the omission is reported on stderr either way.
622 for line
in (previous
or "").splitlines():
623 if not line.startswith(
"| **Ansel**"):
625 cells = [c.strip()
for c
in line.strip().strip(
"|").split(
"|")]
627 for label, value
in zip(labels, cells[1:]):
628 if value
and value
not in (
"—",
"?"):
633 path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"clone_detect.py")
634 spec_cd = importlib.util.spec_from_file_location(
"clone_detect", path)
635 cdm = importlib.util.module_from_spec(spec_cd)
636 spec_cd.loader.exec_module(cdm)
637 _f, ansel_corpus, _t = cdm.scan(source_dir, 20, 12,
True)
639 sub = TREE_DIRS.get(label)
642 root = os.path.join(trees, sub,
"src")
643 if not os.path.isdir(root):
644 sys.stderr.write(
"readme-metrics: no tree for %s at %s\n" % (label, root))
646 _f2, other, _t2 = cdm.scan(root, 20, 12,
True)
647 j = 100.0 * len(ansel_corpus & other) /
max(1, len(ansel_corpus | other))
648 live[label] = round(j, 1)
653 if a ==
"Ansel" or b ==
"Ansel":
654 other = b
if a ==
"Ansel" else a
656 return "%.1f %%" % live[other]
657 return kept.get(other)
658 return "%.1f %%" % FROZEN_SIMILARITY.get((a, b), FROZEN_SIMILARITY.get((b, a), 0.0))
660 out = [
"| | " +
" | ".join(cols) +
" |",
661 "| --- | " +
" | ".join(
"---:" for _
in cols) +
" |"]
662 for i, a
in enumerate(cols):
664 for j, b
in enumerate(cols):
665 value =
"" if j < i
else cell(a, b)
668 "no value for %s x %s and none in the README; pass --darktable-trees"
671 out.append(
"| **" + a +
"** | " +
" | ".join(cells) +
" |")
672 return "\n".join(out) +
"\n"
676 ap = argparse.ArgumentParser(description=__doc__,
677 formatter_class=argparse.RawDescriptionHelpFormatter)
678 ap.add_argument(
"--readme", default=
"README.md")
679 ap.add_argument(
"--doxygen-db",
680 default=
"doc/api/sqlite3/doxygen_sqlite3.db",
681 help=
"Doxygen SQLite symbol table, for documentation coverage; "
682 "produced by the docs build's first pass")
683 ap.add_argument(
"--darktable-trees", default=
None,
684 help=
"directory holding dt38/ dt40/ dt50/ dt56/ Darktable checkouts, "
685 "needed only to refresh the Ansel row of the similarity matrix")
686 ap.add_argument(
"--check", action=
"store_true",
687 help=
"report staleness without writing; non-zero exit if stale")
688 args = ap.parse_args()
690 with open(args.readme, encoding=
"utf-8")
as fh:
696 for m
in CELL.finditer(text):
700 needs = wanted.setdefault(comp, set())
701 if m.group(
"second"):
702 needs.update((
"complexity",
"ncloc"))
706 sys.stderr.write(
"readme-metrics: %d components to refresh\n" % len(wanted))
707 measures, failed, moved = {}, [], {}
708 for i, (comp, metrics)
in enumerate(sorted(wanted.items()), 1):
710 measures[comp] =
fetch(comp, metrics)
711 except Exception
as exc:
714 project, path = comp.split(
":", 1)
718 measures[comp] =
fetch(found, metrics)
720 sys.stderr.write(
"readme-metrics: %s moved to %s\n"
721 % (comp.split(
":")[-1], found.split(
":")[-1]))
722 except Exception
as exc2:
723 failed.append((comp, str(exc2)))
726 failed.append((comp, str(exc)))
729 sys.stderr.write(
"readme-metrics: %d/%d\n" % (i, len(wanted)))
735 have = measures.get(comp, {})
736 old_value, old_second = m.group(
"value"), m.group(
"second")
738 key =
"complexity" if old_second
else metric
739 fresh = have.get(key)
743 new_second = old_second
745 ncloc = have.get(
"ncloc")
746 if ncloc
is not None:
747 new_second =
format_like(old_second, ncloc,
"ncloc")
748 if new_value != old_value
or new_second != old_second:
749 changes.append((comp, key,
750 "%s%s" % (old_value,
" / " + old_second
if old_second
else ""),
751 "%s%s" % (new_value,
" / " + new_second
if new_second
else "")))
754 url = url.replace(urllib.parse.quote(comp, safe=
""),
755 urllib.parse.quote(moved[comp], safe=
""))
756 url = url.replace(comp, moved[comp])
757 out =
"[%s%s](%s)" % (new_value, m.group(
"pct"), url)
759 out +=
" / " + new_second
762 updated = CELL.sub(replace, text)
764 DOXYGEN_DB[0] = args.doxygen_db
765 builders = {
"engine-metrics": engine_table,
766 "engine-complexity": functions_table,
767 "engine-includes": includes_table,
769 sp, args.darktable_trees, prev)}
772 build = builders.get(m.group(
"name"))
776 needs_prev = build
in (builders[
"similarity-matrix"],
777 builders[
"engine-metrics"])
778 body = (build(m.group(
"spec"), m.group(
"body"))
if needs_prev
779 else build(m.group(
"spec")))
780 except Exception
as exc:
781 sys.stderr.write(
"readme-metrics: %s failed (%s), left as is\n"
782 % (m.group(
"name"), exc))
784 if body.strip() != m.group(
"body").strip():
785 changes.append((m.group(
"name"),
"generated block",
"stale",
"refreshed"))
786 return m.group(
"open") + body + m.group(
"close")
788 updated = BLOCK.sub(regenerate, updated)
790 for comp, err
in failed:
791 sys.stderr.write(
"readme-metrics: WARNING could not read %s (%s)\n" % (comp, err))
792 for comp, metric, old, new
in changes:
793 sys.stderr.write(
" %-58s %-22s %s -> %s\n"
794 % (comp.split(
":")[-1], metric, old, new))
795 sys.stderr.write(
"readme-metrics: %d figures changed, %d unreadable\n"
796 % (len(changes), len(failed)))
799 return 1
if changes
else 0
801 with open(args.readme,
"w", encoding=
"utf-8")
as fh:
803 sys.stderr.write(
"readme-metrics: wrote %s\n" % args.readme)