I built a PPT generator. It produced a valid file. PowerPoint opened it. The charts rendered.
But one line was gone, and two bars were missing. Nothing threw an error.
The symptom
I write a tool that generates PPT decks. It runs on Python and pptxgenjs.
The pipeline worked. The file downloaded fine. My checker said everything passed.
Then I opened the file and looked at it:
1.Line chart: should have 4 data points, rendered only 1 line
2.Bar chart: should have 3 groups, lost 2 of them
Nothing anywhere raised an error.
- The generator did not throw
- The file opens
- PowerPoint does not complain
- My own checker reported "all passed" The cause Here is the normalization code: // Wrong: this tests the container type const isSeriesList = rawSeries.length && Array.isArray(rawSeries[0]) ? rawSeries : [rawSeries[0]];
// The data looks like this:
rawSeries = [
{ name: '数量', values: [120, 180, 240] },
{ name: '占比', values: [22, 31, 40] }
]
// Array.isArray(rawSeries[0]) → false (the first item is an object, not an array)
// → the whole array gets wrapped as "one series"
// → 2 series become 1
// → the chart draws only the first line
The file stays valid. The chart stays valid. Half the data is simply gone.
The pattern behind this
Silent degradation ≠ crash
Crash → you know immediately
Silent drop → you think it worked, until a human reads the output
I call this "looks right, is wrong". It is far harder to debug than an exception.
How I debugged it, and why every step was wrong
This part matters more than the bug itself.
- Suspected the dual-axis format → changed it, still broken
- Suspected an options parameter → bisected them all, none was it
- Added a log statement → the combo branch never ran at all
- Ran the normalization alone → saw data.length == 1 Step 3 was the turning point. The branch never ran, so the condition was false. My condition at the time: if (condition) { combo branch } else { plain branch } → combo branch never ran = condition is false → the condition called Array.isArray(...) → go look at what that data actually is Steps 1 to 3 all asked "am I using the library correctly?" Step 4 was the first to ask "is my own logic wrong?" Write the check by meaning, not by shape // Wrong: structural test Array.isArray(r[0]) // a series object is of course not an array
// Also wrong, and this one bites you twice
r.every(x => 'values' in x)
That second one looks right. It is not. Array.prototype.values exists,
so the in operator walks the prototype chain and returns true for any array.
I only found this because I ran the check against both shapes instead of trusting it.
// Right: semantic test, verified against both shapes
!Array.isArray(x) && x && x.values !== undefined
// or: Object.prototype.hasOwnProperty.call(x, 'values')
Both tests ask about the shape of the data. Only one tells them apart:
- [[{...}], [{...}]]: the raw nested form pptxgenjs uses
- [{...}, {...}]: a flat list of series Structurally both are lists. Semantically they are opposite. Lesson I learned the hard way: I wrote the check, ran it, saw green, and stopped. The check was wrong in a way no amount of reading would have shown me. Only feeding it a bad sample exposed it. Three checks I added afterward Fixing the bug was not enough. My checker had to catch this class of problem next time. import re, zipfile
def count_series(xml: str) -> int:
# count('') misses and any namespaced prefix.
return len(re.findall(r'<(?:\w+:)?ser[\s>]', xml))
series_count = 0
with zipfile.ZipFile(pptx_path) as z:
for n in z.namelist():
if 'charts/chart' in n and n.endswith('.xml'):
series_count += count_series(z.read(n).decode('utf-8'))
if series_count < expected_series:
report("chart series were silently dropped")
Do not write xml.count(''). I did. It returns 0 on a real chart,
because the tag carries attributes and a namespace prefix. A check that
always reads zero never fires, and a check that never fires looks identical
to a passing one.
The three checks, running together:
- Series count → regex the XML, compare against expected
- Point alignment → categories must equal data points, otherwise report
- Empty data → all categories or all values empty, refuse to draw Check 1 is the important one. The plots list in python-pptx misses multi-series charts. Only the XML is reliable. Then I made the same mistake again I added the checks. I ran them. Everything was green. I was happy. I was about to stop. Then I did one thing: I built a broken sample the check was supposed to catch. → It reported "all passed". The checks existed. The main flow never called them. The habit now: write the check → confirm the main flow actually calls it → build a bad sample → confirm it gets caught → only then call it done I wrote this rule down. Two days later I broke it again. Writing a rule is not the same as following it. If you write generator tools
- "Generated successfully" does not mean "the content is right". Read the artifact.
- The
inoperator checks the prototype chain, so"key" in []can be true. - Test data shape by semantic feature, not by container type.
- Checks must read the artifact back. Count XML nodes, do not trust the library.
- Silent degradation is harder than an error, because it looks correct.
- Before you report all green, build one broken sample and watch it fail. Number 6 is the whole point of this post. While writing it I checked my own example against both data shapes, and found the "correct" check in my original draft was broken too. Every symptom here comes from my own tool: 2 series collapsing to 1, the four wrong debugging steps, and the three checks I added after.
Top comments (0)