vrmay23 commented on PR #20394:
URL: https://github.com/apache/nuttx/pull/20394#issuecomment-5894340472
The checking script referred to in **Testing**. It is a one-off tool for this
reorganisation — the list of pages with new text is embedded in it on
purpose —
so it is here rather than in the tree.
Save it and run it from the repository root:
```
python3 validate_reorg.py 53ac762e79
```
It exits non-zero and names the page if any page outside the declared list
differs in text once the move is factored out.
```python
#!/usr/bin/env python3
"""Laudo de validacao do branch enhance/documentation (Apache NuttX).
python3 validate_reorg.py [commit-base] # default: ad176ac47f
AFIRMACAO QUE ESTE SCRIPT VERIFICA
----------------------------------
NAO e "so 49 arquivos mudaram". O diff toca mais de mil arquivos.
A afirmacao e mais estreita e mais forte:
De todas as paginas de documentacao, apenas as declaradas abaixo tiveram
PROSA escrita ou
reescrita. Em todas as outras, a mudanca esta confinada a reorganizacao:
alvo de link, caminho, linha de tag, bloco de toctree e borda de tabela.
COMO ELE VERIFICA
-----------------
Nao por amostragem, nem por regex sobre as linhas do diff. Para cada pagina
tocada que NAO esta na lista declarada, ele apaga do texto INTEIRO -- da
versao
antiga e da nova -- tudo que uma reorganizacao mexe, e exige que os dois
resultados sejam byte a byte identicos. Se sobrar uma unica palavra
diferente, a pagina e reportada e o script sai com codigo 1.
A lista esta embutida abaixo de proposito: o marca-texto amarelo que
as destaca no site e andaime de revisao e sai antes do patch, entao o laudo
nao pode depender dele para ser reproduzivel.
"""
import subprocess, sys, re, collections, posixpath, pathlib, os
# A base e o upstream contra o qual este trabalho se mede. Depois de cada
# merge com o upstream ela TEM de avancar para o novo pai upstream do merge,
# senao o laudo passa a contar as mudancas do proprio upstream como se fossem
# nossas -- e reprova. Hoje: 53ac762e79, o pai upstream do merge 0cbcd255e7.
BASE = sys.argv[1] if len(sys.argv) > 1 else "53ac762e79"
# --- as paginas com prosa nova ou reescrita --------------------------------
PROSE_PAGES = {
"ReleaseNotes/index",
"about/index",
"components/index",
"contributing/doc_templates/board",
"developing/index",
"guides/index",
"implementation/index",
"index",
"os/arch/index",
"os/concurrency/index",
"os/drivers/character/bch",
"os/drivers/character/efuse",
"os/drivers/character/ipcc",
"os/drivers/character/leds/index",
"os/drivers/character/loop",
"os/drivers/character/nullzero",
"os/drivers/index",
"os/drivers/special/clk",
"os/drivers/special/devicetree",
"os/drivers/special/pipes",
"os/drivers/special/power/index",
"os/drivers/special/rwbuffer",
"os/drivers/special/syslog",
"os/drivers/special/syslog_design",
"os/drivers/special/usrsock",
"os/filesystem/aio",
"os/filesystem/index",
"os/filesystem/nfs",
"os/filesystem/romfs",
"os/filesystem/userfs",
"os/index",
"os/interrupts/index",
"os/ipc/index",
"os/libs/index",
"os/libs/libm",
"os/memory/index",
"os/memory/paging",
"os/memory/shm",
"os/networking/index",
"os/openamp",
"os/scheduling/index",
"os/scheduling/nuttx_tasking",
"os/scheduling/smp",
"os/time/index",
"os/video",
"platforms/arm/lpc17xx_40xx/index",
"os/libs/libbuiltin",
"platforms/arm/lpc17xx_40xx/lpc40xx",
"platforms/x86_64/intel64/index",
"reference/index",}
# --- paginas divididas em duas ---------------------------------------------
# Um teste por pagina le uma divisao como prosa removida, porque o texto foi
# parar noutro arquivo. Entao a divisao sai do teste geral e e verificada a
# parte: cada linha que saiu de uma metade tem de reaparecer, verbatim e na
# mesma ordem, na outra.
SPLITS = [
("Documentation/implementation/memory_configurations.rst", # original
na base
"Documentation/os/memory/memory_configurations.rst", # metade que
ficou
"Documentation/os/build_modes.rst"), # metade que
saiu
]
# --- helpers ---------------------------------------------------------------
def sh(*a):
return subprocess.run(a, capture_output=True, text=True).stdout
def blob(rev, path):
r = subprocess.run(["git", "show", f"{rev}:{path}"],
capture_output=True, text=True)
return r.stdout if r.returncode == 0 else None
def rule(t=""):
print(("-- " + t + " ").ljust(74, "-") if t else "-" * 74)
# --- normalizacao: apaga tudo que uma reorganizacao mexe, e so isso --------
TAGLINE = re.compile(r"^\s*(\.\.\s+tags::|:tags:).*$")
TOCTREE = re.compile(r"^(\s*)\.\.\s+toctree::")
TOCOPT =
re.compile(r"^\s*:(glob|maxdepth|titlesonly|caption|hidden|numbered|reversed):.*$")
PATHDIR =
re.compile(r"^\s*\.\.\s+(image|figure|include|literalinclude)::.*$")
BORDER = re.compile(r"^\s*[+|][-=+| ]+[+|]\s*$")
UNDERLINE = re.compile(r"^\s*([=\-~^\"#*+`'])\1{2,}\s*$")
def normalise_rst(text):
out, lines, i = [], text.splitlines(), 0
while i < len(lines):
l = lines[i]
m = TOCTREE.match(l)
if m: # navegacao, nao texto
ind = len(m.group(1)); i += 1
while i < len(lines):
nxt = lines[i]
if nxt.strip() and (len(nxt) - len(nxt.lstrip())) <= ind:
break
i += 1
continue
i += 1
if TAGLINE.match(l) or TOCOPT.match(l): continue
if PATHDIR.match(l): out.append("|PATHDIR|"); continue
if BORDER.match(l): out.append("|BORDER|"); continue
if UNDERLINE.match(l): out.append("|RULE|"); continue
l = re.sub(r":(doc|ref|any):`([^`<]*)<[^`>]*>`", r":\1:`\2<>`", l)
l = re.sub(r":(doc|ref|any):`[^`]*`", r":\1:`<>`", l)
l = re.sub(r"`([^`<]*)<[^`>]*>`_+", r"`\1<>`_", l)
l = re.sub(r"https?://\S+", "URL", l)
l = re.sub(r"/?(?:[\w.@-]+/)+[\w.*@-]+", "PATH", l)
l = re.sub(r"\s+", " ", l).strip()
if l: out.append(l)
return "\n".join(out)
def normalise_md(text):
"""Release notes: apaga o que e estrutura de Markdown, deixa as palavras.
A reorganizacao promoveu a primeira linha a titulo de documento
(sublinhado
setext) e, onde o arquivo ja usava '#' para secoes, empurrou todos os
niveis um degrau abaixo para que a release aparecesse como UMA entrada no
indice, e nao uma por secao. Tambem escapou colchetes que o Markdown
comia
e fechou cercas de codigo. Nada disso e palavra."""
out = []
for l in text.splitlines():
if re.match(r"^\s*[=-]{3,}\s*$", l): continue # sublinhado
setext
if re.match(r"^\s*```", l): continue # cerca de codigo
l = re.sub(r"^\s*#+\s*", "|H| ", l) # nivel de titulo
l = l.replace("\\", "") # escapes de
Markdown
l = re.sub(r"\s+", " ", l).strip()
if l: out.append(l)
return "\n".join(out)
# --- 1. o que o git mostra -------------------------------------------------
head = sh("git", "rev-parse", "--short", "HEAD").strip()
stat = sh("git", "-c", "diff.renameLimit=20000", "diff", "--find-renames",
"--shortstat", BASE, "HEAD").strip()
print()
rule()
print(f" LAUDO base={BASE} head={head}")
rule()
print(f"\n git diff --find-renames --stat {BASE} HEAD")
print(f" {stat}\n")
status = sh("git", "-c", "diff.renameLimit=20000", "diff", "--find-renames",
"--name-status", BASE, "HEAD")
entries = []
for line in status.splitlines():
p = line.split("\t")
entries.append((p[0], p[1], p[-1]))
nat = collections.Counter()
for st, a, b in entries:
if st == "R100": nat["movido, conteudo identico"] += 1
elif st == "A": nat["adicionado"] += 1
elif st == "D": nat["removido"] += 1
else: nat["conteudo alterado"] += 1
rule("1. OS ARQUIVOS DO DIFF, POR NATUREZA")
for k, v in nat.most_common(): print(f" {k:<32} {v:>5}")
print(f" {'TOTAL':<32} {sum(nat.values()):>5}")
# --- 2. paginas .rst -------------------------------------------------------
pair = {}
for st, a, b in entries:
if not b.endswith(".rst"): continue
if st.startswith("R"): pair[b] = a
elif st == "M": pair[b] = a
head_rst = [f for f in sh("git", "ls-files", "Documentation").split() if
f.endswith(".rst")]
SPLIT_HALVES = {h for _, a, b in SPLITS for h in (a, b)}
cat, suspeitas = collections.Counter(), []
for new in head_rst:
d = new[len("Documentation/"):-4]
if new in SPLIT_HALVES:
cat["metade de uma pagina dividida (secao 3)"] += 1; continue
if d in PROSE_PAGES:
cat[f"prosa escrita ou reescrita (as {len(PROSE_PAGES)})"] += 1;
continue
old = pair.get(new)
if old is None:
cat["nunca tocada"] += 1; continue
a, b = blob(BASE, old), blob("HEAD", new)
if a == b:
cat["nunca tocada" if old == new else "movida, byte a byte
identica"] += 1; continue
if normalise_rst(a) == normalise_rst(b):
cat["alterada, so reorganizacao"] += 1
else:
cat["*** SOBROU PROSA ***"] += 1; suspeitas.append((d, old))
# Paginas .rst NOVAS que nao estao declaradas. Uma pagina adicionada nao tem
# versao anterior, entao o teste de residuo acima nao a alcanca: se este
script
# apenas presumisse que ela nao tem prosa, bastaria esquecer de declarar uma
# pagina para o laudo aprovar um texto que ninguem verificou. Por isso cada
# uma e TESTADA: sobrou linha de prosa depois de tirar titulo, diretiva,
opcao
# de diretiva e entrada de toctree, a pagina e prosa nao declarada, e o laudo
# reprova.
def tem_prosa(caminho):
linhas = pathlib.Path(caminho).read_text(errors="replace").splitlines()
resto, i = [], 0
while i < len(linhas):
ln = linhas[i]
nxt = linhas[i + 1] if i + 1 < len(linhas) else ""
if re.fullmatch(r"[=~^\"'*+#<>_-]{3,}", ln.strip()):
i += 1; continue # sublinhado de titulo
if ln.strip() and re.fullmatch(r"[=~^\"'*+#<>_-]{3,}", nxt.strip()):
i += 2; continue # o titulo e seu
sublinhado
if ln.lstrip().startswith(".."):
i += 1; continue # diretiva ou comentario
if re.fullmatch(r"\s+:[\w-]+:.*", ln):
i += 1; continue # opcao de diretiva
if re.fullmatch(r"\s+\S+", ln) and " " not in ln.strip():
i += 1; continue # entrada de toctree
if ln.strip():
resto.append(ln.strip())
i += 1
return resto
novas = [f for st, a, f in entries
if st == "A" and f.endswith(".rst")
and f[len("Documentation/"):-4] not in PROSE_PAGES
and f not in SPLIT_HALVES]
sem_prosa = []
for f in novas:
prosa = tem_prosa(f) if os.path.exists(f) else []
if prosa:
cat["*** PROSA NAO DECLARADA ***"] += 1
suspeitas.append((f[len("Documentation/"):-4],
f"pagina nova com {len(prosa)} linhas de prosa, "
f"fora da lista declarada"))
else:
sem_prosa.append(f)
cat["nova, sem prosa (so toctree)"] += len(sem_prosa)
for f in novas:
if f in head_rst: cat["nunca tocada"] -= 1
rule(f"2. PAGINAS .rst NO HEAD ({len(head_rst)})")
for k, v in cat.most_common():
if v: print(f" {k:<38} {v:>5}")
print(f" {'TOTAL':<38} {sum(v for v in cat.values()):>5}")
print(f"\n removidas de vez (nao existem no HEAD): "
f"{sum(1 for st,a,b in entries if st=='D' and b.endswith('.rst'))}")
# --- 3. paginas divididas
---------------------------------------------------
import difflib
rule(f"3. PAGINAS DIVIDIDAS EM DUAS ({len(SPLITS)})")
split_fail = []
for src_old, kept, moved in SPLITS:
o = (blob(BASE, src_old) or "").splitlines()
k = (blob("HEAD", kept) or "").splitlines()
m = (blob("HEAD", moved) or "").splitlines()
saiu = []
for tag, i1, i2, j1, j2 in difflib.SequenceMatcher(None, o, k,
autojunk=False).get_opcodes():
if tag in ("delete", "replace"): saiu.extend(o[i1:i2])
sm = difflib.SequenceMatcher(None, saiu, m, autojunk=False)
verbatim = sum(i2 - i1 for tag, i1, i2, j1, j2 in sm.get_opcodes() if
tag == "equal")
perdidas = [l for tag, i1, i2, j1, j2 in sm.get_opcodes()
if tag in ("delete", "replace") for l in saiu[i1:i2]]
novas = [l for tag, i1, i2, j1, j2 in sm.get_opcodes()
if tag in ("insert", "replace") for l in m[j1:j2]]
print(f" {src_old[len('Documentation/'):]} ({len(o)} linhas)")
print(f" -> {kept[len('Documentation/'):]} ({len(k)} linhas)")
print(f" -> {moved[len('Documentation/'):]} ({len(m)} linhas)")
print(f" linhas que sairam da primeira:
{len(saiu):>4}")
print(f" reaparecem verbatim na segunda:
{verbatim:>4}")
print(f" perdidas: "
f"{len([l for l in perdidas if l.strip()]):>4}"
f" (+ {len([l for l in perdidas if not l.strip()])} em branco)")
print(f" acrescentadas (titulo e rotulo da pagina):
{len(novas):>3}")
for l in novas: print(f" + {l[:66]}")
if [l for l in perdidas if l.strip()]:
split_fail.append(moved)
# --- 4. release notes (.md) ------------------------------------------------
md = [(a, b) for st, a, b in entries if b.endswith(".md") and st != "R100"]
md_ok, md_extra = 0, []
for a, b in md:
x, y = blob(BASE, a), blob("HEAD", b)
if normalise_md(x) == normalise_md(y): md_ok += 1
else: md_extra.append(b)
rule(f"4. RELEASE NOTES .md ({len(md)})")
print(f" so a promocao do titulo (sublinhado setext) {md_ok:>5}")
print(f" com alguma correcao alem disso
{len(md_extra):>5}")
for f in md_extra: print(f" {f[len('Documentation/'):]}")
# --- 5. arquivos que nao sao pagina ---------------------------------------
PORQUE = {
".github/workflows/doc.yml":
"roda check_doc_coverage.py no CI e observa boards/",
"Documentation/Pipfile":
"declara sphinx-reredirects",
"Documentation/Pipfile.lock":
"trava sphinx-reredirects; sem isto o 'pipenv sync' do CI falharia",
"Documentation/_extensions/review_highlight.py":
"ANDAIME DE REVISAO: pinta as declaradas de amarelo. Sai antes do patch",
"Documentation/_extensions/tags_overview.py":
"reagrupa o indice de tags em arch > chip > part",
"Documentation/_static/custom.css":
"estilo do indice de tags, prosa justificada, e as regras do andaime",
"Documentation/_templates/layout.html":
"conserta o logo sumido: o tema renomeou 'logo' para 'logo_url'",
"Documentation/conf.py":
"extensoes, redirects, ano do copyright derivado do build",
"Documentation/platforms/chip-vendors.txt":
"85 regras familia -> fabricante, usadas pelo CI",
"Documentation/platforms/doc-coverage-ignore.txt":
"38 boards sem pagina, debito rastreado",
"Documentation/redirects.py":
"514 redirects, caminhos calculados; preserva as URLs antigas",
"tools/ci/check_doc_coverage.py":
"verificacao nova: boards/ e Documentation/platforms/ tem de bater",
}
outros = [(st, b) for st, a, b in entries
if not b.endswith((".rst", ".md")) and st != "R100"]
rule(f"5. ARQUIVOS QUE NAO SAO PAGINA ({len(outros)})")
for st, f in sorted(outros, key=lambda x: x[1]):
porque = PORQUE.get(f, "diagrama SVG autoral" if f.endswith(".svg") else
"")
print(f" {st:<3} {f}")
if porque: print(f" {porque}")
# --- 6. veredito -----------------------------------------------------------
print()
rule()
if split_fail:
print(" REPROVADO: uma divisao perdeu texto:")
for f in split_fail: print(f" {f}")
rule()
sys.exit(1)
if suspeitas:
print(f" REPROVADO: {len(suspeitas)} pagina(s) fora da lista declarada
com texto alem")
print(" de reorganizacao:")
for d, o in suspeitas: print(f" {d}")
rule()
sys.exit(1)
print(" APROVADO -- a afirmacao verificada e sobre CONTEUDO")
print()
print(f" Fora das {len(PROSE_PAGES)} paginas declaradas, nenhuma palavra de
prosa")
print(" foi escrita, reescrita ou removida, com UMA excecao, nomeada aqui:
o")
print(" titulo da release note NuttX-0.2.9, que dizia 'NuttX-0.2.8'. Typo")
print(" herdado do upstream, corrigido na Fase 1.")
print()
print(" O que mudou fora delas, e um leitor percebe:")
print(" . para onde os links apontam (mesma pagina de destino, caminho
novo)")
print(" . quais tags uma pagina de board carrega")
print(" . o nivel dos titulos nas release notes, para a release virar
UMA")
print(" entrada no indice em vez de uma por secao")
print(" . a navegacao: onde cada pagina fica na arvore")
print()
print(" Isto NAO afirma que so 49 arquivos mudaram. O diff toca 1026,
porque")
print(" a reestruturacao foi grande. Afirma que o TEXTO mudou em 49
paginas,")
print(" e que elas estao nomeadas, uma a uma, na lista embutida neste
script.")
rule()
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]