Skip to content

Commit c172741

Browse files
authored
test(live): add Docs structure-preservation regressions (#841)
Co-authored-by: Sebastian Roth <[email protected]>
1 parent 83f51c6 commit c172741

2 files changed

Lines changed: 197 additions & 4 deletions

File tree

scripts/live-test.sh

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ usage() {
1616
Usage: scripts/live-test.sh [options]
1717
1818
Options:
19-
--fast Skip slower tests (docs/sheets/slides)
19+
--fast Skip slower tests (docs/sheets/slides/structure)
2020
--strict Fail on optional tests (groups/keep/enterprise)
2121
--allow-nontest Allow running against non-test accounts
2222
--account <email> Account to use (defaults to GOG_IT_ACCOUNT or first auth)
@@ -29,7 +29,7 @@ Skip keys (base):
2929
time, version, completion, schema, help, output-precedence, auth, auth-alias, config, enable-commands,
3030
gmail, gmail-settings, gmail-delegates, gmail-batch-delete, gmail-history, gmail-url, gmail-labels,
3131
gmail-send-safety, gmail-forward,
32-
gmail-attachments, gmail-track, gmail-watch, chat, drive, docs, sheets, slides,
32+
gmail-attachments, gmail-track, gmail-watch, chat, drive, docs, sheets, slides, structure,
3333
photos-picker,
3434
calendar, calendar-enterprise, calendar-respond, calendar-team, calendar-users,
3535
tasks, contacts, contacts-directory, contacts-other, people, groups, keep, classroom
@@ -109,9 +109,9 @@ fi
109109
SKIP="${SKIP:-${GOG_LIVE_SKIP:-}}"
110110
if [ "$FAST" = true ]; then
111111
if [ -n "$SKIP" ]; then
112-
SKIP="$SKIP,docs,sheets,slides"
112+
SKIP="$SKIP,docs,sheets,slides,structure"
113113
else
114-
SKIP="docs,sheets,slides"
114+
SKIP="docs,sheets,slides,structure"
115115
fi
116116
fi
117117

@@ -155,6 +155,7 @@ source "$ROOT_DIR/scripts/live-tests/drive.sh"
155155
source "$ROOT_DIR/scripts/live-tests/docs.sh"
156156
source "$ROOT_DIR/scripts/live-tests/sheets.sh"
157157
source "$ROOT_DIR/scripts/live-tests/slides.sh"
158+
source "$ROOT_DIR/scripts/live-tests/structure.sh"
158159
source "$ROOT_DIR/scripts/live-tests/photos.sh"
159160
source "$ROOT_DIR/scripts/live-tests/calendar.sh"
160161
source "$ROOT_DIR/scripts/live-tests/tasks.sh"
@@ -187,6 +188,7 @@ run_drive_tests
187188
run_docs_tests
188189
run_sheets_tests
189190
run_slides_tests
191+
run_structure_tests
190192
run_photos_picker_tests
191193
run_calendar_tests
192194
run_tasks_tests

scripts/live-tests/structure.sh

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
# Docs structure-preservation regressions. Target paragraphs contain STRUCT_;
6+
# every other paragraph is fingerprinted before edits and compared after each
7+
# mutation so adjacent content, order, paragraph structure, and run styles stay
8+
# unchanged.
9+
10+
docs_structure_fingerprint() {
11+
local raw="$1"
12+
"$PY" -c '
13+
import json,sys
14+
obj=json.load(sys.stdin)
15+
out=[]
16+
for el in obj.get("body",{}).get("content",[]):
17+
p=el.get("paragraph")
18+
if not p:
19+
continue
20+
runs=[]
21+
text=""
22+
for item in p.get("elements",[]):
23+
run=item.get("textRun")
24+
if not run:
25+
continue
26+
content=run.get("content","")
27+
text+=content
28+
runs.append({"text":content,"style":run.get("textStyle",{})})
29+
if "STRUCT_" in text or not text.strip():
30+
continue
31+
style={k:v for k,v in p.get("paragraphStyle",{}).items() if k != "headingId"}
32+
out.append({"text":text,"bullet":p.get("bullet"),"style":style,"runs":runs})
33+
json.dump(out,sys.stdout,sort_keys=True,separators=(",",":"))' <<<"$raw"
34+
}
35+
36+
assert_docs_structure_fingerprint() {
37+
local before="$1" raw="$2" after
38+
after=$(docs_structure_fingerprint "$raw")
39+
if [ "$after" != "$before" ]; then
40+
echo "Untargeted Docs structure changed" >&2
41+
BEFORE="$before" AFTER="$after" "$PY" -c '
42+
import difflib,json,os
43+
before=json.dumps(json.loads(os.environ["BEFORE"]),indent=2,sort_keys=True).splitlines()
44+
after=json.dumps(json.loads(os.environ["AFTER"]),indent=2,sort_keys=True).splitlines()
45+
print("\n".join(difflib.unified_diff(before,after,fromfile="before",tofile="after")))' >&2
46+
return 1
47+
fi
48+
}
49+
50+
docs_paragraph_property() {
51+
local raw="$1" needle="$2" property="$3"
52+
NEEDLE="$needle" PROPERTY="$property" "$PY" -c '
53+
import json,os,sys
54+
obj=json.load(sys.stdin); needle=os.environ["NEEDLE"]; prop=os.environ["PROPERTY"]
55+
for el in obj.get("body",{}).get("content",[]):
56+
p=el.get("paragraph")
57+
if not p:
58+
continue
59+
text="".join(r.get("textRun",{}).get("content","") for r in p.get("elements",[]))
60+
if needle not in text:
61+
continue
62+
if prop == "bullet.listId":
63+
print(p.get("bullet",{}).get("listId", ""))
64+
elif prop == "paragraphStyle.namedStyleType":
65+
print(p.get("paragraphStyle",{}).get("namedStyleType", ""))
66+
elif prop == "text":
67+
print(text, end="")
68+
sys.exit(0)
69+
raise SystemExit(f"paragraph {needle!r} not found")' <<<"$raw"
70+
}
71+
72+
assert_docs_paragraph_property() {
73+
local raw="$1" needle="$2" property="$3" want="$4" got
74+
got=$(docs_paragraph_property "$raw" "$needle" "$property")
75+
[ "$got" = "$want" ] || {
76+
echo "Paragraph $needle property $property: got '$got', want '$want'" >&2
77+
return 1
78+
}
79+
}
80+
81+
assert_docs_run_style() {
82+
local raw="$1" needle="$2" property="$3" want="$4"
83+
NEEDLE="$needle" PROPERTY="$property" WANT="$want" "$PY" -c '
84+
import json,os,sys
85+
obj=json.load(sys.stdin); needle=os.environ["NEEDLE"]; prop=os.environ["PROPERTY"]; want=os.environ["WANT"]
86+
def walk(value):
87+
if isinstance(value,dict):
88+
if "textRun" in value:
89+
yield value["textRun"]
90+
for child in value.values():
91+
yield from walk(child)
92+
elif isinstance(value,list):
93+
for child in value:
94+
yield from walk(child)
95+
for run in walk(obj):
96+
if run.get("content","").strip() != needle:
97+
continue
98+
style=run.get("textStyle",{})
99+
got=style.get("link",{}).get("url","") if prop == "link.url" else style.get(prop)
100+
expected=True if want == "true" else want
101+
assert got == expected, f"run {needle!r} property {prop}: {got!r} != {expected!r}"
102+
sys.exit(0)
103+
raise SystemExit(f"run {needle!r} not found")' <<<"$raw"
104+
}
105+
106+
assert_docs_inline_image_count() {
107+
local raw="$1" want="$2"
108+
WANT="$want" "$PY" -c '
109+
import json,os,sys
110+
obj=json.load(sys.stdin)
111+
def count(value):
112+
if isinstance(value,dict):
113+
return (1 if "inlineObjectElement" in value else 0) + sum(count(v) for v in value.values())
114+
if isinstance(value,list):
115+
return sum(count(v) for v in value)
116+
return 0
117+
got=count(obj); want=int(os.environ["WANT"])
118+
assert got == want, f"inline image count {got} != {want}"' <<<"$raw"
119+
}
120+
121+
run_structure_tests() {
122+
if skip "structure"; then
123+
echo "==> structure (skipped)"
124+
return 0
125+
fi
126+
127+
local doc_json doc_id seed_path block_path heading_path before baseline after
128+
local inline_list_id block_list_id image_url
129+
doc_json=$(gog docs create "gogcli-structure-doc-$TS" --json)
130+
doc_id=$(extract_id "$doc_json")
131+
[ -n "$doc_id" ] || { echo "Failed to parse structure doc id" >&2; exit 1; }
132+
register_drive_cleanup "$doc_id"
133+
134+
seed_path="$LIVE_TMP/structure-seed-$TS.md"
135+
block_path="$LIVE_TMP/structure-block-$TS.md"
136+
heading_path="$LIVE_TMP/structure-heading-$TS.md"
137+
printf '# Heading control\n\nNormal with **bold word** and *italic word* and a [link word](https://example.com) inside.\n\n## STRUCT_HEADING_TARGET\n\nHeading neighbor.\n\n- Item one\n- STRUCT_INLINE_TARGET\n- STRUCT_BLOCK_TARGET\n- Item four\n\n1. First step\n2. Second step\n3. Third step\n\nSTRUCT_IMAGE_TARGET\n\nPlain trailing paragraph.\n' >"$seed_path"
138+
printf 'STRUCT_BLOCK_TARGET A\n\nSTRUCT_BLOCK_TARGET B' >"$block_path"
139+
printf 'STRUCT_HEADING_TARGET A\n\nSTRUCT_HEADING_TARGET B' >"$heading_path"
140+
141+
run_required "structure" "structure docs seed" gog docs write "$doc_id" \
142+
--file "$seed_path" --replace --markdown --json >/dev/null
143+
before=$(gog docs raw "$doc_id" --json)
144+
baseline=$(docs_structure_fingerprint "$before")
145+
inline_list_id=$(docs_paragraph_property "$before" "STRUCT_INLINE_TARGET" "bullet.listId")
146+
block_list_id=$(docs_paragraph_property "$before" "STRUCT_BLOCK_TARGET" "bullet.listId")
147+
[ -n "$inline_list_id" ] && [ -n "$block_list_id" ] || {
148+
echo "Structure fixture list IDs missing" >&2
149+
exit 1
150+
}
151+
assert_docs_run_style "$before" "bold word" "bold" "true"
152+
assert_docs_run_style "$before" "italic word" "italic" "true"
153+
assert_docs_run_style "$before" "link word" "link.url" "https://example.com"
154+
155+
run_required "structure" "structure docs inline markdown replacement" \
156+
gog docs find-replace "$doc_id" "STRUCT_INLINE_TARGET" "STRUCT_INLINE_TARGET edited" \
157+
--first --format markdown --json >/dev/null
158+
after=$(gog docs raw "$doc_id" --json)
159+
assert_docs_paragraph_property "$after" "STRUCT_INLINE_TARGET edited" "bullet.listId" "$inline_list_id"
160+
assert_docs_structure_fingerprint "$baseline" "$after"
161+
162+
run_required "structure" "structure docs block markdown list replacement" \
163+
gog docs find-replace "$doc_id" "STRUCT_BLOCK_TARGET" --content-file "$block_path" \
164+
--first --format markdown --json >/dev/null
165+
after=$(gog docs raw "$doc_id" --json)
166+
assert_docs_paragraph_property "$after" "STRUCT_BLOCK_TARGET A" "bullet.listId" "$block_list_id"
167+
assert_docs_paragraph_property "$after" "STRUCT_BLOCK_TARGET B" "bullet.listId" ""
168+
assert_docs_structure_fingerprint "$baseline" "$after"
169+
170+
run_required "structure" "structure docs block markdown heading replacement" \
171+
gog docs find-replace "$doc_id" "STRUCT_HEADING_TARGET" --content-file "$heading_path" \
172+
--first --format markdown --json >/dev/null
173+
after=$(gog docs raw "$doc_id" --json)
174+
assert_docs_paragraph_property "$after" "STRUCT_HEADING_TARGET A" "paragraphStyle.namedStyleType" "HEADING_2"
175+
assert_docs_paragraph_property "$after" "STRUCT_HEADING_TARGET B" "paragraphStyle.namedStyleType" "NORMAL_TEXT"
176+
assert_docs_structure_fingerprint "$baseline" "$after"
177+
178+
image_url="https://www.gstatic.com/images/branding/product/2x/docs_96dp.png"
179+
run_required "structure" "structure docs image before preserved anchor" \
180+
gog docs insert-image "$doc_id" --url "$image_url" --before "STRUCT_IMAGE_TARGET" \
181+
--width 40 --json >/dev/null
182+
run_required "structure" "structure docs image after preserved anchor" \
183+
gog docs insert-image "$doc_id" --url "$image_url" --after "STRUCT_IMAGE_TARGET" \
184+
--width 40 --json >/dev/null
185+
after=$(gog docs raw "$doc_id" --json)
186+
assert_docs_paragraph_property "$after" "STRUCT_IMAGE_TARGET" "text" "STRUCT_IMAGE_TARGET"
187+
assert_docs_inline_image_count "$after" 2
188+
assert_docs_structure_fingerprint "$baseline" "$after"
189+
190+
run_required "structure" "structure docs delete" gog drive delete "$doc_id" --force >/dev/null
191+
}

0 commit comments

Comments
 (0)