xlsxrb 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.devcontainer/Dockerfile +2 -1
- data/.gem_rbs_collection/ast/2.4/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/ast/2.4/ast.rbs +73 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/array.rbs +4 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/atomic_reference.rbs +16 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/executor.rbs +96 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/hash.rbs +4 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/map.rbs +68 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/promises.rbs +249 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/set.rbs +4 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/timer_task.rbs +47 -0
- data/.gem_rbs_collection/concurrent-ruby/1.1/utility/processor_counter.rbs +5 -0
- data/.gem_rbs_collection/csv/3.3/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/csv/3.3/csv.rbs +3871 -0
- data/.gem_rbs_collection/csv/3.3/manifest.yaml +3 -0
- data/.gem_rbs_collection/lint_roller/1.1/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/lint_roller/1.1/lint_roller.rbs +48 -0
- data/.gem_rbs_collection/listen/3.9/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/listen/3.9/listen.rbs +25 -0
- data/.gem_rbs_collection/listen/3.9/listener.rbs +24 -0
- data/.gem_rbs_collection/logger/1.7/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/logger/1.7/formatter.rbs +45 -0
- data/.gem_rbs_collection/logger/1.7/log_device.rbs +100 -0
- data/.gem_rbs_collection/logger/1.7/logger.rbs +796 -0
- data/.gem_rbs_collection/logger/1.7/manifest.yaml +2 -0
- data/.gem_rbs_collection/logger/1.7/period.rbs +17 -0
- data/.gem_rbs_collection/logger/1.7/severity.rbs +34 -0
- data/.gem_rbs_collection/parallel/1.20/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/parallel/1.20/parallel.rbs +86 -0
- data/.gem_rbs_collection/parser/3.2/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/parser/3.2/manifest.yaml +7 -0
- data/.gem_rbs_collection/parser/3.2/parser.rbs +194 -0
- data/.gem_rbs_collection/parser/3.2/polyfill.rbs +4 -0
- data/.gem_rbs_collection/rainbow/3.0/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rainbow/3.0/global.rbs +7 -0
- data/.gem_rbs_collection/rainbow/3.0/presenter.rbs +209 -0
- data/.gem_rbs_collection/rainbow/3.0/rainbow.rbs +5 -0
- data/.gem_rbs_collection/rake/13.0/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rake/13.0/manifest.yaml +2 -0
- data/.gem_rbs_collection/rake/13.0/rake.rbs +39 -0
- data/.gem_rbs_collection/regexp_parser/2.8/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/regexp_parser/2.8/regexp_parser.rbs +17 -0
- data/.gem_rbs_collection/rubocop/1.57/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rubocop/1.57/rubocop.rbs +208 -0
- data/.gem_rbs_collection/rubocop-ast/1.46/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rubocop-ast/1.46/rubocop-ast.rbs +903 -0
- data/CHANGELOG.md +24 -3
- data/README.md +91 -17
- data/Rakefile +124 -89
- data/Steepfile +19 -0
- data/docs/ARCHITECTURE.md +17 -0
- data/docs/DEVELOPMENT.md +58 -0
- data/docs/QUALITY_ASSURANCE.md +26 -0
- data/docs/visual/VisualGallery.md +893 -938
- data/docs/wasm/ruby.wasm +0 -0
- data/lib/xlsxrb/elements/cell.rb +74 -1
- data/lib/xlsxrb/elements/column.rb +2 -0
- data/lib/xlsxrb/elements/row.rb +37 -2
- data/lib/xlsxrb/elements/types.rb +2 -0
- data/lib/xlsxrb/elements/workbook.rb +29 -0
- data/lib/xlsxrb/elements/worksheet.rb +98 -1
- data/lib/xlsxrb/elements.rb +2 -0
- data/lib/xlsxrb/ooxml/reader.rb +8 -6
- data/lib/xlsxrb/ooxml/shared_strings_parser.rb +2 -0
- data/lib/xlsxrb/ooxml/styles_parser.rb +2 -0
- data/lib/xlsxrb/ooxml/utils.rb +2 -0
- data/lib/xlsxrb/ooxml/workbook_parser.rb +6 -1
- data/lib/xlsxrb/ooxml/workbook_writer.rb +15 -4
- data/lib/xlsxrb/ooxml/worksheet_parser.rb +12 -0
- data/lib/xlsxrb/ooxml/worksheet_writer.rb +49 -9
- data/lib/xlsxrb/ooxml/writer.rb +217 -5
- data/lib/xlsxrb/ooxml/xml_builder.rb +2 -0
- data/lib/xlsxrb/ooxml/xml_parser.rb +2 -0
- data/lib/xlsxrb/ooxml/zip_generator.rb +5 -8
- data/lib/xlsxrb/ooxml/zip_reader.rb +88 -52
- data/lib/xlsxrb/ooxml/zip_writer.rb +2 -0
- data/lib/xlsxrb/ooxml.rb +3 -1
- data/lib/xlsxrb/style_builder.rb +156 -36
- data/lib/xlsxrb/version.rb +3 -1
- data/lib/xlsxrb.rb +1510 -231
- data/rbs_collection.lock.yaml +224 -0
- data/rbs_collection.yaml +19 -0
- data/sig/generated/xlsxrb/elements.rbs +8 -0
- data/sig/generated/xlsxrb/ooxml.rbs +23 -0
- data/sig/generated/xlsxrb/style_builder.rbs +88 -0
- data/sig/generated/xlsxrb/version.rbs +5 -0
- metadata +67 -29
- data/benchmark.rb +0 -399
- data/measure_memory.rb +0 -42
- data/sig/xlsxrb.rbs +0 -23
data/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,33 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
|
+
- No unreleased changes.
|
|
2
3
|
|
|
4
|
+
## [0.1.5] - 2026-08-11
|
|
5
|
+
|
|
6
|
+
### Added
|
|
7
|
+
- Functional API for updating existing files (`Xlsxrb.modify`).
|
|
8
|
+
- New `Workbook#update_sheet` and `Worksheet#update_cell` helpers for immutable data structures.
|
|
9
|
+
- Syntactic sugar for `styles:` in `sheet.row` using `Hash` and `Range` keys (e.g. `styles: { 0..4 => "header" }`).
|
|
10
|
+
- Support for inline anonymous styles as Hash objects (e.g. `styles: { 0 => { font: { bold: true } } }`).
|
|
11
|
+
- Support for `Range` and `Array` arguments in `sheet.column` to modify multiple columns at once.
|
|
12
|
+
- `WorkbookBuilder#[]` and `StreamWriter#[]` aliased to `#sheet` for elegant context switching.
|
|
13
|
+
- Block-based configuration for `font` and `border` properties inside `StyleBuilder`.
|
|
14
|
+
- Extensive inline RBS typing with strict generic types (eliminated `untyped` from the public API).
|
|
15
|
+
- Full `RBS::Test` runtime type validation enabled for the entire test suite.
|
|
16
|
+
- Extensive Excel limit warnings documented via YARD tags.
|
|
17
|
+
- Formal SemVer API contract with `@api public` tags for user-facing methods.
|
|
18
|
+
- Comprehensive mutation testing (Mutant) and test coverage (SimpleCov) integrations.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- Replaced `method_missing` with statically defined, fully typed methods in `WorksheetProxy`, `ChartBuilder`, and `SeriesBuilder`.
|
|
22
|
+
- Removed deprecated `instance_eval` block context for builders; explicit block arguments are now required.
|
|
23
|
+
- Hardened security and bounds checking across the DSL for `strict_excel_mode`.
|
|
24
|
+
- Fixed numerous Rubocop linting violations and standardized style rules.
|
|
3
25
|
- Add specification reference policy and implemented specification mapping (`docs/SPEC_SOURCES.md`).
|
|
4
26
|
- Introduce unified event-based streaming and parsing architecture (`Ooxml::Event` and event streams for WorksheetParser/SharedStringsParser).
|
|
5
|
-
- Restructure test suite into four distinct tiers (Unit, Contract, E2E, Visual).
|
|
6
|
-
- Add visual examples gallery (`examples/visual/` and `docs/visual/README.md`).
|
|
27
|
+
- Restructure test suite into four distinct tiers (Unit, Contract, E2E, Visual) and renamed `facade_test.rb` to `public_api_test.rb`.
|
|
28
|
+
- Add visual examples gallery (`examples/visual/` and `docs/visual/README.md`) and promoted it via animated GIF in the main README.
|
|
7
29
|
- Implement visual regression testing (VRT) pipeline comparing generated sheets against reference baselines.
|
|
8
30
|
|
|
9
|
-
|
|
10
31
|
## [0.1.0] - 2026-03-25
|
|
11
32
|
|
|
12
33
|
- Initial release
|
data/README.md
CHANGED
|
@@ -77,11 +77,11 @@ Generate large files efficiently by writing data directly to the file stream:
|
|
|
77
77
|
```ruby
|
|
78
78
|
require "xlsxrb"
|
|
79
79
|
|
|
80
|
-
Xlsxrb.generate("large_output.xlsx") do |
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
Xlsxrb.generate("large_output.xlsx") do |wb|
|
|
81
|
+
wb.sheet("Sales Data") do |sheet|
|
|
82
|
+
sheet.row(["Date", "Amount", "Status"])
|
|
83
|
+
sheet.row([Date.today, 100, true])
|
|
84
|
+
sheet.column(0, width: 15.5)
|
|
85
85
|
end
|
|
86
86
|
end
|
|
87
87
|
```
|
|
@@ -91,12 +91,56 @@ Read rows one at a time without loading the entire file into memory:
|
|
|
91
91
|
```ruby
|
|
92
92
|
require "xlsxrb"
|
|
93
93
|
|
|
94
|
-
Xlsxrb.foreach("large_file.xlsx"
|
|
95
|
-
|
|
94
|
+
Xlsxrb.foreach("large_file.xlsx") do |sheet|
|
|
95
|
+
sheet.each do |row|
|
|
96
|
+
puts "Row #{row.index}: #{row.cells.map(&:value).join(', ')}"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### In-Memory Building & Modifying
|
|
102
|
+
|
|
103
|
+
`xlsxrb` provides a powerful, immutable-by-default API for modifying existing Excel files or building templates in-memory.
|
|
104
|
+
|
|
105
|
+
#### Modifying an Existing File
|
|
106
|
+
You can update specific cells or sheets using the functional `Xlsxrb.modify` API, which yields the parsed `Workbook`.
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
require "xlsxrb"
|
|
110
|
+
|
|
111
|
+
# Create a dummy template.xlsx for this example
|
|
112
|
+
Xlsxrb.build { |w| w.sheet("Invoice") }.write("template.xlsx")
|
|
113
|
+
|
|
114
|
+
Xlsxrb.modify("template.xlsx", "output.xlsx") do |wb|
|
|
115
|
+
wb.update_sheet("Invoice") do |sheet|
|
|
116
|
+
# Update a specific cell
|
|
117
|
+
sheet = sheet.update_cell("C4", value: "INV-10042")
|
|
118
|
+
sheet = sheet.update_cell("C5", value: Date.today)
|
|
119
|
+
|
|
120
|
+
# Or append new rows
|
|
121
|
+
sheet.with(rows: sheet.rows + [
|
|
122
|
+
Xlsxrb::Elements::Row.new(index: sheet.rows.size, cells: [])
|
|
123
|
+
])
|
|
124
|
+
end
|
|
96
125
|
end
|
|
97
126
|
```
|
|
98
127
|
|
|
99
|
-
|
|
128
|
+
#### Hash & Range Styling (Syntactic Sugar)
|
|
129
|
+
You can directly apply inline styles or use Ranges for multiple columns without boilerplate:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
Xlsxrb.build do |wb|
|
|
133
|
+
# Use [] accessor for sheets
|
|
134
|
+
wb["Report"].row(
|
|
135
|
+
["ID", "Name", "Score", "Rank"],
|
|
136
|
+
# Apply 'header' style to first two columns, and bold inline style to the third
|
|
137
|
+
styles: { 0..1 => "header", 2 => { font: { bold: true, color: "red" } } }
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Set multiple column widths at once using Ranges
|
|
141
|
+
wb["Report"].column("A".."D", width: 15.0)
|
|
142
|
+
end
|
|
143
|
+
```
|
|
100
144
|
|
|
101
145
|
## Feature Support & ECMA-376 Compliance
|
|
102
146
|
|
|
@@ -145,28 +189,58 @@ To run the benchmark locally for 1,000,000 cells (100,000 rows):
|
|
|
145
189
|
ruby benchmark.rb 100000
|
|
146
190
|
```
|
|
147
191
|
|
|
192
|
+
## Security (Protection against CSV/Excel Injection)
|
|
193
|
+
|
|
194
|
+
Unlike CSV files which lack type definitions and force Excel to guess types (often inadvertently executing strings starting with `=`), `.xlsx` files generated by `xlsxrb` are strictly typed.
|
|
195
|
+
|
|
196
|
+
When you pass a Ruby `String` to `xlsxrb`, it explicitly writes it as a `String` (`t="s"`) into the OOXML file. Therefore, **even if a string starts with `=`, Excel will never evaluate it as a formula**. To write a formula, you must explicitly use `Xlsxrb::Elements::Formula.new`. This design completely mitigates CSV/Formula Injection vulnerabilities by default without requiring additional sanitization.
|
|
197
|
+
|
|
198
|
+
### External Link Updates (`update_links`)
|
|
199
|
+
|
|
200
|
+
As an extra layer of "defense in depth", `xlsxrb` configures the workbook to **never automatically update external links** when opened (`updateLinks="never"`). This is intentionally set to `never` by default to prevent Excel from silently reaching out to external resources or executing DDE (Dynamic Data Exchange) links, which is a known vector for malware.
|
|
201
|
+
|
|
202
|
+
If you absolutely need external links to update automatically, you can explicitly override this (though **it is highly discouraged due to security risks**):
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
Xlsxrb.generate("file.xlsx") do |wb|
|
|
206
|
+
# WARNING: Enabling this can expose users to malicious external reference vulnerabilities!
|
|
207
|
+
wb.workbook_property(:update_links, "always")
|
|
208
|
+
# ...
|
|
209
|
+
end
|
|
210
|
+
```
|
|
211
|
+
|
|
148
212
|
## Testing & Quality Assurance
|
|
149
213
|
|
|
150
|
-
To support reliability, compliance with the ECMA-376 specification, and consistent updates, `xlsxrb` is backed by a
|
|
214
|
+
To support reliability, compliance with the ECMA-376 specification, and consistent updates, `xlsxrb` is backed by a highly rigorous, enterprise-grade Quality Assurance (QA) and testing architecture.
|
|
215
|
+
|
|
216
|
+
### Multi-Tier Testing Strategy
|
|
217
|
+
* **Round-Trip Testing**: Unit tests verify that every generated sheet can be reliably parsed back by the reader with identical content and styling.
|
|
218
|
+
* **Contract Consistency**: Ensures semantic output consistency between the Streaming (`Xlsxrb.generate`) and In-Memory (`Xlsxrb.build`) APIs.
|
|
219
|
+
* **Property-Based Testing (PBT)**: Automatically generates random data to catch edge cases (e.g., huge numbers, special characters) preventing unexpected crashes.
|
|
220
|
+
* **Concurrency Validation**: Thread and Ractor safety checks to guarantee no global variable pollution during parallel execution.
|
|
221
|
+
* **Security & DoS Protection**: Hardened against malicious files, including memory exhaustion (ZIP Bombs) and infinite parsing loops.
|
|
222
|
+
|
|
223
|
+
### Strict Interoperability & Rendering
|
|
224
|
+
* **Official Open XML SDK Validation (E2E)**: Every generated spreadsheet is structurally validated against the official Microsoft Open XML SDK to prevent file corruption warnings in Microsoft Excel.
|
|
225
|
+
* **Visual Regression Testing (VRT)**: Spreadsheets are rendered via a headless LibreOffice Calc engine and compared pixel-by-pixel against visual baselines to catch subtle rendering regressions.
|
|
151
226
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
4. Visual Regression Testing (VRT): To guarantee rendering correctness, generated XLSX files are rendered via a headless LibreOffice Calc engine, and compared pixel-by-pixel against visual baselines.
|
|
227
|
+
### Performance & Types
|
|
228
|
+
* **Continuous Benchmarking**: Memory usage and processing speeds are profiled in CI on large datasets to prevent performance regressions and OOM leaks.
|
|
229
|
+
* **Runtime Type Validation**: Strong dynamic typing using `RBS::Test` to ensure the library's types are perfectly sound at runtime.
|
|
156
230
|
|
|
157
|
-
For
|
|
231
|
+
For a comprehensive breakdown of our QA matrix, see [docs/QUALITY_ASSURANCE.md](docs/QUALITY_ASSURANCE.md). For details on running tests locally, see [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md).
|
|
158
232
|
|
|
159
233
|
## Development
|
|
160
234
|
|
|
161
235
|
We welcome contributions! The project is configured with a ready-to-use Dev Container to streamline local environment setup.
|
|
162
236
|
|
|
163
|
-
For contribution guidelines, E2E testing policies, and the step-by-step development workflow, please refer to [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md).
|
|
237
|
+
For contribution guidelines, E2E testing policies, and the step-by-step development workflow (including how to run the Dev Container from your terminal), please refer to [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md).
|
|
164
238
|
|
|
165
239
|
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
166
240
|
|
|
167
241
|
## Contributing
|
|
168
242
|
|
|
169
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/niku/xlsxrb. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](
|
|
243
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/niku/xlsxrb. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](CODE_OF_CONDUCT.md).
|
|
170
244
|
|
|
171
245
|
## License
|
|
172
246
|
|
|
@@ -174,4 +248,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
|
174
248
|
|
|
175
249
|
## Code of Conduct
|
|
176
250
|
|
|
177
|
-
Everyone interacting in the Xlsxrb project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](
|
|
251
|
+
Everyone interacting in the Xlsxrb project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](CODE_OF_CONDUCT.md).
|
data/Rakefile
CHANGED
|
@@ -88,6 +88,12 @@ end
|
|
|
88
88
|
task test: %i[build_sdk_runner ensure_reader_fixtures]
|
|
89
89
|
|
|
90
90
|
namespace :test do
|
|
91
|
+
desc "Run tests with runtime type checking enabled (RBS_TEST=1)"
|
|
92
|
+
task :rbs do
|
|
93
|
+
ENV["RBS_TEST"] = "1"
|
|
94
|
+
Rake::Task["test:unit"].invoke
|
|
95
|
+
Rake::Task["test:contract"].invoke
|
|
96
|
+
end
|
|
91
97
|
Rake::TestTask.new(:unit) do |t|
|
|
92
98
|
t.libs << "test"
|
|
93
99
|
t.libs << "lib"
|
|
@@ -139,7 +145,10 @@ require "rubocop/rake_task"
|
|
|
139
145
|
|
|
140
146
|
RuboCop::RakeTask.new
|
|
141
147
|
|
|
142
|
-
task
|
|
148
|
+
require "bundler/audit/task"
|
|
149
|
+
Bundler::Audit::Task.new
|
|
150
|
+
|
|
151
|
+
task default: %i[bundle:audit rubocop typecheck test]
|
|
143
152
|
|
|
144
153
|
namespace :visual do
|
|
145
154
|
desc "Generate docs/visual/ README.md explanation gallery"
|
|
@@ -192,11 +201,13 @@ task :wasm do
|
|
|
192
201
|
require "open-uri"
|
|
193
202
|
wasm_url = "https://cdn.jsdelivr.net/npm/@ruby/4.0-wasm-wasi@2.9.3-2.9.4/dist/ruby.wasm"
|
|
194
203
|
FileUtils.mkdir_p(File.dirname(original_wasm_cache))
|
|
204
|
+
# rubocop:disable Security/Open
|
|
195
205
|
URI.open(wasm_url) do |stream|
|
|
196
206
|
File.open(original_wasm_cache, "wb") do |file|
|
|
197
207
|
IO.copy_stream(stream, file)
|
|
198
208
|
end
|
|
199
209
|
end
|
|
210
|
+
# rubocop:enable Security/Open
|
|
200
211
|
puts "Original ruby.wasm cached successfully."
|
|
201
212
|
end
|
|
202
213
|
|
|
@@ -208,6 +219,8 @@ task :wasm do
|
|
|
208
219
|
|
|
209
220
|
# A. Write custom static stubs and gateways dynamically
|
|
210
221
|
File.write(File.join(bundle_assets_dir, "openssl.rb"), "# frozen_string_literal: true\n")
|
|
222
|
+
time_rb_path = $LOAD_PATH.lazy.map { |p| File.join(p, "time.rb") }.find { |f| File.exist?(f) }
|
|
223
|
+
FileUtils.cp(time_rb_path, bundle_assets_dir) if time_rb_path
|
|
211
224
|
|
|
212
225
|
File.write(File.join(bundle_assets_dir, "opentelemetry.rb"), <<~RUBY)
|
|
213
226
|
# frozen_string_literal: true
|
|
@@ -231,9 +244,7 @@ task :wasm do
|
|
|
231
244
|
|
|
232
245
|
# Resolve and copy host's rexml files
|
|
233
246
|
rexml_spec_path = $LOAD_PATH.find { |p| File.exist?(File.join(p, "rexml/rexml.rb")) }
|
|
234
|
-
if rexml_spec_path
|
|
235
|
-
FileUtils.cp_r(File.join(rexml_spec_path, "rexml"), bundle_assets_dir)
|
|
236
|
-
end
|
|
247
|
+
FileUtils.cp_r(File.join(rexml_spec_path, "rexml"), bundle_assets_dir) if rexml_spec_path
|
|
237
248
|
|
|
238
249
|
# Gateway for strscan
|
|
239
250
|
File.write(File.join(bundle_assets_dir, "strscan.rb"), <<~RUBY)
|
|
@@ -259,11 +270,11 @@ task :wasm do
|
|
|
259
270
|
]
|
|
260
271
|
stdlib_files.each do |name|
|
|
261
272
|
path = $LOAD_PATH.find { |p| File.exist?(File.join(p, name)) }
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
273
|
+
next unless path
|
|
274
|
+
|
|
275
|
+
dest_path = File.join(bundle_assets_dir, name)
|
|
276
|
+
FileUtils.mkdir_p(File.dirname(dest_path))
|
|
277
|
+
FileUtils.cp(File.join(path, name), dest_path)
|
|
267
278
|
end
|
|
268
279
|
|
|
269
280
|
# C. Patch tmpdir.rb to automatically create /tmp in Wasm virtual filesystem (since Wasm has no writable /tmp by default)
|
|
@@ -286,92 +297,104 @@ task :wasm do
|
|
|
286
297
|
puts "Building packed ruby.wasm from staging bundle..."
|
|
287
298
|
cmd = "bundle exec rbwasm pack #{original_wasm_cache} --dir #{wasm_bundle_dir}::/usr/local/lib/ruby/site_ruby -o #{packed_wasm_path}"
|
|
288
299
|
puts "Executing: #{cmd}"
|
|
289
|
-
unless system(cmd)
|
|
290
|
-
raise "Failed to build packed ruby.wasm using rbwasm pack!"
|
|
291
|
-
end
|
|
300
|
+
raise "Failed to build packed ruby.wasm using rbwasm pack!" unless system(cmd)
|
|
292
301
|
end
|
|
293
302
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
require 'fileutils'
|
|
303
|
+
def download_file(url, dest)
|
|
304
|
+
# Check if the file exists and is not a tiny placeholder/error document
|
|
305
|
+
return if File.exist?(dest) && File.size(dest) > 1024
|
|
298
306
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
return if File.exist?(dest) && File.size(dest) > 1024
|
|
307
|
+
puts "Downloading #{url} to #{dest}..."
|
|
308
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
302
309
|
|
|
303
|
-
|
|
304
|
-
|
|
310
|
+
require "net/http"
|
|
311
|
+
uri = URI.parse(url)
|
|
312
|
+
temp_dest = "#{dest}.tmp"
|
|
305
313
|
|
|
306
|
-
|
|
307
|
-
uri
|
|
308
|
-
|
|
314
|
+
begin
|
|
315
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 15, read_timeout: 90) do |http|
|
|
316
|
+
request = Net::HTTP::Get.new(uri)
|
|
317
|
+
# Specify User-Agent to bypass scraping prevention on CDNs and act as a normal browser
|
|
318
|
+
request["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
319
|
+
# Force raw (identity) encoding to prevent receiving Brotli-compressed (.br) data,
|
|
320
|
+
# which Ruby's Net::HTTP cannot decode automatically, leading to corrupted Wasm files.
|
|
321
|
+
request["Accept-Encoding"] = "identity"
|
|
309
322
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
request = Net::HTTP::Get.new(uri)
|
|
313
|
-
# Specify User-Agent to bypass scraping prevention on CDNs and act as a normal browser
|
|
314
|
-
request['User-Agent'] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
315
|
-
# Force raw (identity) encoding to prevent receiving Brotli-compressed (.br) data,
|
|
316
|
-
# which Ruby's Net::HTTP cannot decode automatically, leading to corrupted Wasm files.
|
|
317
|
-
request['Accept-Encoding'] = 'identity'
|
|
318
|
-
|
|
319
|
-
http.request(request) do |response|
|
|
320
|
-
if response.code.to_i != 200
|
|
321
|
-
raise "HTTP error #{response.code}: #{response.message}"
|
|
322
|
-
end
|
|
323
|
+
http.request(request) do |response|
|
|
324
|
+
raise "HTTP error #{response.code}: #{response.message}" if response.code.to_i != 200
|
|
323
325
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
end
|
|
326
|
+
File.open(temp_dest, "wb") do |output|
|
|
327
|
+
response.read_body do |chunk|
|
|
328
|
+
output.write(chunk)
|
|
328
329
|
end
|
|
329
330
|
end
|
|
330
331
|
end
|
|
331
|
-
# Atomic rename to prevent leaving incomplete files on failure
|
|
332
|
-
File.rename(temp_dest, dest)
|
|
333
|
-
puts "Downloaded successfully."
|
|
334
|
-
rescue => e
|
|
335
|
-
puts "Failed to download #{url}: #{e.message}"
|
|
336
|
-
File.delete(temp_dest) if File.exist?(temp_dest)
|
|
337
|
-
File.delete(dest) if File.exist?(dest)
|
|
338
|
-
raise "Required asset download failed. Build aborted."
|
|
339
332
|
end
|
|
333
|
+
# Atomic rename to prevent leaving incomplete files on failure
|
|
334
|
+
File.rename(temp_dest, dest)
|
|
335
|
+
puts "Downloaded successfully."
|
|
336
|
+
|
|
337
|
+
# Dynamic Brotli Decompression if the server ignored identity encoding and sent Brotli (.br) data
|
|
338
|
+
if File.exist?(dest) && File.binread(dest, 4)&.bytes == [0xCF, 0xFF, 0xFF, 0x7F]
|
|
339
|
+
puts "Detected Brotli compression on #{dest}. Decompressing..."
|
|
340
|
+
|
|
341
|
+
unpacked = "#{dest}.unpacked"
|
|
342
|
+
if system("brotli -d -f -o #{unpacked} #{dest}")
|
|
343
|
+
File.rename(unpacked, dest)
|
|
344
|
+
puts "Decompressed #{dest} successfully."
|
|
345
|
+
else
|
|
346
|
+
FileUtils.rm_f(unpacked)
|
|
347
|
+
raise "Failed to decompress Brotli file: #{dest}"
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
rescue StandardError => e
|
|
351
|
+
puts "Failed to download #{url}: #{e.message}"
|
|
352
|
+
FileUtils.rm_f(temp_dest)
|
|
353
|
+
FileUtils.rm_f(dest)
|
|
354
|
+
raise "Required asset download failed. Build aborted."
|
|
340
355
|
end
|
|
356
|
+
end
|
|
341
357
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
358
|
+
def fetch_google_fonts
|
|
359
|
+
css_dest = "docs/fonts/fonts.css"
|
|
360
|
+
return if File.exist?(css_dest)
|
|
345
361
|
|
|
346
|
-
|
|
347
|
-
|
|
362
|
+
puts "Fetching and localizing Google Fonts..."
|
|
363
|
+
font_url = "https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&family=JetBrains+Mono:wght@400;500&display=swap"
|
|
348
364
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
rescue => e
|
|
357
|
-
puts "Failed to fetch Google Fonts CSS: #{e.message}"
|
|
358
|
-
return
|
|
365
|
+
css_content = nil
|
|
366
|
+
begin
|
|
367
|
+
# Specify Chrome User-Agent to ensure Google Fonts returns modern and lightweight .woff2 formats
|
|
368
|
+
# instead of legacy formats (like .ttf or .eot) designed for older browsers
|
|
369
|
+
# rubocop:disable Security/Open
|
|
370
|
+
URI.open(font_url, "User-Agent" => "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") do |f|
|
|
371
|
+
css_content = f.read
|
|
359
372
|
end
|
|
373
|
+
# rubocop:enable Security/Open
|
|
374
|
+
rescue StandardError => e
|
|
375
|
+
puts "Failed to fetch Google Fonts CSS: #{e.message}"
|
|
376
|
+
return
|
|
377
|
+
end
|
|
360
378
|
|
|
361
|
-
|
|
379
|
+
urls = css_content.scan(%r{url\((https://fonts\.gstatic\.com/[^)]+)\)}).flatten
|
|
362
380
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
end
|
|
369
|
-
|
|
370
|
-
FileUtils.mkdir_p("docs/fonts")
|
|
371
|
-
File.write(css_dest, css_content)
|
|
372
|
-
puts "Google Fonts localized successfully."
|
|
381
|
+
urls.uniq.each do |url|
|
|
382
|
+
filename = url.split("/").last
|
|
383
|
+
local_path = "docs/fonts/#{filename}"
|
|
384
|
+
download_file(url, local_path)
|
|
385
|
+
css_content.gsub!(url, filename)
|
|
373
386
|
end
|
|
374
387
|
|
|
388
|
+
FileUtils.mkdir_p("docs/fonts")
|
|
389
|
+
File.write(css_dest, css_content)
|
|
390
|
+
puts "Google Fonts localized successfully."
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
desc "Fetch required external assets for offline usage"
|
|
394
|
+
task :fetch_assets do
|
|
395
|
+
require "open-uri"
|
|
396
|
+
require "fileutils"
|
|
397
|
+
|
|
375
398
|
# Fetch Ruby WASI JS
|
|
376
399
|
download_file(
|
|
377
400
|
"https://cdn.jsdelivr.net/npm/@ruby/wasm-wasi@2.9.3-2.9.4/dist/browser.umd.js",
|
|
@@ -389,16 +412,16 @@ task :fetch_assets do
|
|
|
389
412
|
end
|
|
390
413
|
|
|
391
414
|
desc "Generate RDoc documentation including Visual Gallery"
|
|
392
|
-
task doc: [
|
|
415
|
+
task doc: %i[wasm fetch_assets] do
|
|
393
416
|
FileUtils.rm_rf("doc")
|
|
394
417
|
|
|
395
418
|
# RDoc コマンドを実行 (--exclude を指定して docs 配下のプレビュー用アセットの誤パースを回避)
|
|
396
|
-
sh "bundle exec rdoc --op doc" \
|
|
397
|
-
"
|
|
398
|
-
"
|
|
399
|
-
"
|
|
400
|
-
"
|
|
401
|
-
"
|
|
419
|
+
sh "bundle exec rdoc --op doc " \
|
|
420
|
+
"--exclude 'docs/coi-serviceworker\\.js' " \
|
|
421
|
+
"--exclude 'docs/zeta\\.js' " \
|
|
422
|
+
"--exclude 'docs/office_thread\\.js' " \
|
|
423
|
+
"--exclude 'docs/preview\\.html' " \
|
|
424
|
+
"--title 'xlsxrb Documentation' --main README.md README.md CHANGELOG.md CODE_OF_CONDUCT.md docs/*.md \"docs/visual/VisualGallery.md\" lib/"
|
|
402
425
|
|
|
403
426
|
# Copy visual gallery images and files so they are available in RDoc output
|
|
404
427
|
FileUtils.mkdir_p("doc/test/visual/baselines")
|
|
@@ -435,13 +458,14 @@ task doc: [:wasm, :fetch_assets] do
|
|
|
435
458
|
# Inject stylesheet and javascript loading tags to all generated HTML docs
|
|
436
459
|
Dir.glob("doc/**/*.html").each do |html_path|
|
|
437
460
|
next if File.basename(html_path) == "preview.html"
|
|
461
|
+
|
|
438
462
|
html_content = File.read(html_path)
|
|
439
463
|
depth = html_path.sub(%r{\Adoc/}, "").count("/")
|
|
440
464
|
rel_prefix = "../" * depth
|
|
441
465
|
|
|
442
|
-
coi_tag = %
|
|
443
|
-
js_tag = %
|
|
444
|
-
css_tag = %
|
|
466
|
+
coi_tag = %(<script src="#{rel_prefix}coi-serviceworker.js"></script>)
|
|
467
|
+
js_tag = %(<script src="#{rel_prefix}js/wasm_doc_helper.js" defer></script>)
|
|
468
|
+
css_tag = %(<link href="#{rel_prefix}css/wasm_doc_helper.css" rel="stylesheet">)
|
|
445
469
|
|
|
446
470
|
if html_content.include?("<body")
|
|
447
471
|
modified = html_content.sub("<body", "#{coi_tag}\n#{js_tag}\n#{css_tag}\n<body")
|
|
@@ -467,15 +491,17 @@ namespace :doc do
|
|
|
467
491
|
AccessLog: [],
|
|
468
492
|
# If the file is a Brotli-compressed Wasm/Data asset (checked via magic bytes),
|
|
469
493
|
# dynamically inject 'Content-Encoding: br' header so the browser decompresses it natively.
|
|
470
|
-
RequestCallback:
|
|
494
|
+
RequestCallback: lambda { |req, res|
|
|
471
495
|
if req.path.end_with?(".wasm") || req.path.end_with?(".data")
|
|
472
496
|
# Resolve physical file path from req.path manually since res.filename is nil at this stage
|
|
473
497
|
local_path = File.join(File.expand_path("doc", __dir__), req.path)
|
|
474
498
|
if File.exist?(local_path)
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
499
|
+
first_bytes = begin
|
|
500
|
+
File.binread(local_path, 4)
|
|
501
|
+
rescue StandardError
|
|
502
|
+
nil
|
|
478
503
|
end
|
|
504
|
+
res["Content-Encoding"] = "br" if first_bytes != "\x00asm"
|
|
479
505
|
end
|
|
480
506
|
end
|
|
481
507
|
}
|
|
@@ -499,3 +525,12 @@ namespace :doc do
|
|
|
499
525
|
end
|
|
500
526
|
end
|
|
501
527
|
end
|
|
528
|
+
desc "Generate RBS signature files from inline annotations"
|
|
529
|
+
task :sig do
|
|
530
|
+
sh "bundle exec rbs-inline --output lib/**/*.rb"
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
desc "Run static type checking with Steep"
|
|
534
|
+
task typecheck: :sig do
|
|
535
|
+
sh "bundle exec steep check"
|
|
536
|
+
end
|
data/Steepfile
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
target :lib do
|
|
4
|
+
signature "sig"
|
|
5
|
+
|
|
6
|
+
check "lib/xlsxrb.rb"
|
|
7
|
+
|
|
8
|
+
library "date"
|
|
9
|
+
library "time"
|
|
10
|
+
library "securerandom"
|
|
11
|
+
library "openssl"
|
|
12
|
+
library "pathname"
|
|
13
|
+
library "tempfile"
|
|
14
|
+
library "bigdecimal"
|
|
15
|
+
|
|
16
|
+
repo_path "sig"
|
|
17
|
+
|
|
18
|
+
configure_code_diagnostics(Steep::Diagnostic::Ruby.lenient)
|
|
19
|
+
end
|
data/docs/ARCHITECTURE.md
CHANGED
|
@@ -18,6 +18,23 @@ Xlsxrb uses **only** the Ruby standard library and Bundled Gems:
|
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
21
|
+
## Coding Policies
|
|
22
|
+
|
|
23
|
+
### 🚫 The `method_missing` Policy
|
|
24
|
+
As a strict rule, **we do not accept dynamic method definitions using `method_missing`** anywhere in the codebase. All user-facing API methods and internal delegations must be explicitly defined in the code. This ensures:
|
|
25
|
+
1. **Type Safety & Static Analysis**: RBS and Steep can completely validate arguments and structures.
|
|
26
|
+
2. **Developer Experience**: IDE autocompletion, jump-to-definition, and YARD documentation work perfectly.
|
|
27
|
+
3. **Traceability**: If a method exists, you can `grep` for it.
|
|
28
|
+
|
|
29
|
+
Even in cases where proxy patterns (e.g. `WorksheetProxy`) or OOXML builder mappings (e.g. `ChartBuilder`, `SeriesBuilder`) would traditionally benefit from dynamic method delegation to avoid boilerplate, we explicitly generate and write out those delegations in the source code.
|
|
30
|
+
|
|
31
|
+
### API Contract (SemVer)
|
|
32
|
+
- Methods tagged with `@api public` in their YARD documentation are guaranteed to follow SemVer.
|
|
33
|
+
- Methods without this tag (or tagged `@api private`) are internal and may change at any time.
|
|
34
|
+
- Note that during the 0.x series, this is a "best effort" promise, but after 1.0.0, strict SemVer guarantees will apply to all `@api public` methods.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
21
38
|
## Directory Structure
|
|
22
39
|
|
|
23
40
|
```
|
data/docs/DEVELOPMENT.md
CHANGED
|
@@ -65,3 +65,61 @@ Omission is only acceptable when **all** of the following hold:
|
|
|
65
65
|
3. Unit tests and round-trip tests fully cover the new behaviour.
|
|
66
66
|
4. `rake test` passes with Open XML SDK validation included.
|
|
67
67
|
5. The commit message explicitly names the existing E2E scenario that provides coverage and states why a new scenario adds no value.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Development via Dev Container
|
|
72
|
+
|
|
73
|
+
The project is pre-configured with a Dev Container to simplify local environment setup (installing .NET, LibreOffice, ImageMagick, and Noto fonts).
|
|
74
|
+
|
|
75
|
+
### VS Code (GUI)
|
|
76
|
+
You can open this repository in VS Code and select **"Dev Containers: Reopen in Container"** from the Command Palette.
|
|
77
|
+
|
|
78
|
+
### Terminal (Devcontainer CLI)
|
|
79
|
+
If you prefer to use the terminal instead of VS Code, you can run the devcontainer using the official `@devcontainers/cli`:
|
|
80
|
+
|
|
81
|
+
1. Install the CLI on your host machine (if not already installed):
|
|
82
|
+
```bash
|
|
83
|
+
npm install -g @devcontainers/cli
|
|
84
|
+
```
|
|
85
|
+
2. Use the helper script `bin/devcontainer` to start and interact with the container:
|
|
86
|
+
- **Interactive shell** (Automatically shares your host's Git/AI tool configs as read-only):
|
|
87
|
+
```bash
|
|
88
|
+
bin/devcontainer
|
|
89
|
+
```
|
|
90
|
+
- **Run commands directly**:
|
|
91
|
+
```bash
|
|
92
|
+
bin/devcontainer rake test
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Customizing with Personal Overrides (e.g., Dotfiles, API Keys, Shell History)
|
|
96
|
+
|
|
97
|
+
The `bin/devcontainer` script dynamically parses a personal configuration patch and maps it to devcontainer CLI flags on startup. This is a workaround for a specification limit of devcontainer CLI's `--override-config` flag, which acts as a complete replacement of the configuration rather than a partial merge, wiping out base configurations like build settings.
|
|
98
|
+
|
|
99
|
+
To configure this, create a JSON (or JSONC) file at `${XDG_CONFIG_HOME:-~/.config}/devcontainer/override.jsonc` (or `override.json`) on your host machine.
|
|
100
|
+
|
|
101
|
+
**Example: Persisting histories, forwarding API keys, and Auto-installing Dotfiles**
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"mounts": [
|
|
105
|
+
// Volume for general persistent directories (e.g., Claude Code history in ~/.local/share/claude)
|
|
106
|
+
"type=volume,source=xlsxrb-local-share,target=/home/vscode/.local/share"
|
|
107
|
+
],
|
|
108
|
+
"containerEnv": {
|
|
109
|
+
// Explicitly forward API keys from your host shell to the container environment
|
|
110
|
+
"OPENAI_API_KEY": "${localEnv:OPENAI_API_KEY}",
|
|
111
|
+
"ANTHROPIC_API_KEY": "${localEnv:ANTHROPIC_API_KEY}",
|
|
112
|
+
"GEMINI_API_KEY": "${localEnv:GEMINI_API_KEY}"
|
|
113
|
+
},
|
|
114
|
+
"dotfiles": {
|
|
115
|
+
"repository": "https://github.com/<your-github-username>/dotfiles.git",
|
|
116
|
+
"targetPath": "~/dotfiles",
|
|
117
|
+
"installCommand": "install.sh"
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
You can also use a custom file path by exporting the `DEVCONTAINER_OVERRIDE_CONFIG` environment variable in your host shell:
|
|
123
|
+
```bash
|
|
124
|
+
export DEVCONTAINER_OVERRIDE_CONFIG="/path/to/your/custom-override.json"
|
|
125
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Quality Assurance (QA) & Testing Architecture
|
|
2
|
+
|
|
3
|
+
`xlsxrb` is designed to be an enterprise-grade, highly reliable, and highly performant library for reading and writing Excel spreadsheets. To achieve and maintain this standard, we have implemented a comprehensive Quality Assurance matrix that covers everything from static code analysis and dynamic runtime validation to performance benchmarking and visual regression testing.
|
|
4
|
+
|
|
5
|
+
Below is an overview of the inspection mechanisms, when they run, the quality attributes they guarantee, and the specific bugs they prevent.
|
|
6
|
+
|
|
7
|
+
## QA Matrix
|
|
8
|
+
|
|
9
|
+
| Inspection Mechanism / Tool | Execution Command / Mechanism | Local | CI (PR/Push) | Scheduled (Weekly) | Target Quality Attribute | Validation Method | Prevented Bugs / Issues |
|
|
10
|
+
| :--- | :--- | :---: | :---: | :---: | :--- | :--- | :--- |
|
|
11
|
+
| **RuboCop** | `rake rubocop` | ⭕ | ⭕ | - | Readability / Maintainability | Static Analysis (Syntax/Linting) | Overly complex methods, non-standard syntax, unused variables. |
|
|
12
|
+
| **Steep & RBS** | `rake typecheck` | ⭕ | ⭕ | - | Type Safety (Static) | Static Analysis (Type Check) | `NoMethodError`, passing incorrect arguments, method typo bugs. |
|
|
13
|
+
| **Bundler Audit** | `rake audit` | - | ⭕ | - | Security | Dependency Scanning | Inclusion of external gems with known vulnerabilities (CVEs). |
|
|
14
|
+
| **Dependabot** | `.github/dependabot.yml` | - | - | ⭕ | Currency / Maintenance | Repository Monitoring | Outdated dependencies or CI actions. |
|
|
15
|
+
| **Unit & Contract Tests** | `rake test:unit test:contract` | ⭕ | ⭕ | - | Accuracy / Functional Reqs | Dynamic Analysis (Assertions) | Method specification violations, unexpected return values, edge-case failures. |
|
|
16
|
+
| **Runtime Type Validation (RBS::Test)** | `rake test:rbs` | △ (Opt-in) | ⭕ | - | Type Safety (Dynamic) | Dynamic Analysis (Runtime Hooks) | Type errors slipping past static checks, divergence between RBS docs and implementation. |
|
|
17
|
+
| **Property-Based Testing (PBT)** | Included in `rake test:unit` | ⭕ | ⭕ | - | Robustness / Exhaustiveness | Automated Random Generation | Crashes caused by "unexpected inputs" (e.g., empty strings, huge numbers, special symbols like `=`). |
|
|
18
|
+
|
|
19
|
+
| **Security Validation (DoS Protection)** | Included in `rake test:unit` | ⭕ | ⭕ | - | Availability / Safety | Dynamic Analysis (Malicious Input) | Memory/disk exhaustion from ZIP bombs, infinite parsing loops from malformed files. |
|
|
20
|
+
| **Concurrency Validation (Thread/Ractor)** | Included in `rake test:unit` | ⭕ | ⭕ | - | Thread Safety | Dynamic Analysis (Parallel Execution) | Global variable pollution, data mixing during concurrent request processing. |
|
|
21
|
+
| **XSD Schema Validation** | Included in `rake test:unit` | ⭕ | ⭕ | - | Compatibility / Compliance | Structural Validation | "We found a problem with some content in it" errors when opening in Excel. |
|
|
22
|
+
| **E2E Interoperability Tests** | `rake test:e2e` | △ (Opt-in) | ⭕ | - | Compatibility (Real-world) | 3rd-party SDK Execution | Structural defects so severe that the official .NET SDK cannot read them. |
|
|
23
|
+
| **Load / Stress Testing** | Action: `benchmark.yml` | - | ⭕ | - | Performance / Stability | Massive Data Generation (e.g., 10k+ rows) | Out of Memory (OOM) crashes or extreme delays when processing large datasets. |
|
|
24
|
+
| **Memory & Speed Benchmark** | Action: `benchmark.yml` | - | ⭕ | - | Performance | Continuous Profiling | Memory leaks, severe performance degradation due to inefficient loop additions. |
|
|
25
|
+
| **Visual Regression Testing (VRT)** | `rake test:visual` | - | ⭕ | - | Visual Accuracy (UI/UX) | Headless Rendering / Pixel Diff | Visual bugs like "cell background colors dropping" or "chart layouts breaking" after code changes. |
|
|
26
|
+
|