xrpn 2.5 → 2.6
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/README.md +29 -4
- data/tests/README.md +259 -0
- data/tests/run_tests.rb +220 -0
- data/xcmd/rawinfo +22 -0
- data/xlib/_xrpn_version +1 -1
- data/xlib/raw_info +155 -0
- data/xrpn.gemspec +2 -2
- metadata +9 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0451ad69d90e115070029b6619c12eeea1c3470044080d656b4e7130ff5a7ce2
|
|
4
|
+
data.tar.gz: ac6027e1bf40f409fb5be3fa75ab7dbadecc934ec7a5a3165c6dd82d21b94bd0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8a67e64e4e2e63fcc712c21d8520a726e753a8978ada1e3c7b56f52cf8611faa63b351df020251ed784ccc402d95ae3413186c20396dcc9325bee966ae548ab6
|
|
7
|
+
data.tar.gz: 6621b4407c422896602022105fc38e159b247ee6856b263fea01366a30c1f1bbeb1951df5c449cc24c999df483b19ff8ae348503fce4cddd4f836e4df1f55ccb
|
data/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# XRPN
|
|
2
|
-
 [](https://badge.fury.io/rb/xrpn)  
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
 [](https://badge.fury.io/rb/xrpn)  
|
|
5
4
|
|
|
6
|
-
XRPN is a stack-based programming language, similar to [Forth](https://en.wikipedia.org/wiki/Forth_(programming_language)), but simpler in nature.
|
|
5
|
+
<img src="img/xrpn_logo.svg" align="left" width="150" height="150"> XRPN is a stack-based programming language, similar to [Forth](https://en.wikipedia.org/wiki/Forth_(programming_language)), but simpler in nature.
|
|
6
|
+
<br clear="left"/>
|
|
7
7
|
|
|
8
8
|
XRPN is on-the-fly extensible. Language functions can be upgraded or implemented while programs are running.
|
|
9
9
|
|
|
@@ -79,9 +79,34 @@ AVIEW
|
|
|
79
79
|
END
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
+
## HP-41 RAW File Support
|
|
83
|
+
|
|
84
|
+
XRPN can now inspect HP-41 RAW format program files. Use the `RAWINFO` command to view labels, strings, and hex dump of RAW files.
|
|
85
|
+
|
|
86
|
+
**Usage:**
|
|
87
|
+
```
|
|
88
|
+
xrpn
|
|
89
|
+
> "/path/to/program.raw"
|
|
90
|
+
> rawinfo
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
This displays:
|
|
94
|
+
- All program labels (global and local)
|
|
95
|
+
- Text strings found in the program
|
|
96
|
+
- Hex dump for analysis
|
|
97
|
+
|
|
98
|
+
**Note:** Full RAW-to-XRPN conversion requires comprehensive HP-41 bytecode documentation and is planned for future releases. The current implementation provides informational viewing of RAW files.
|
|
99
|
+
|
|
82
100
|
## Changelog
|
|
83
101
|
|
|
84
|
-
### Version 2.
|
|
102
|
+
### Version 2.6 (Latest)
|
|
103
|
+
**New Features and Testing**
|
|
104
|
+
|
|
105
|
+
- **Added HP-41 RAW file viewer** - New `RAWINFO` command displays labels, strings, and hex dump from HP-41 RAW program files
|
|
106
|
+
- **Comprehensive regression test framework** - 58 automated tests across 7 command categories with 100% pass rate
|
|
107
|
+
- **Test infrastructure** - YAML-based test specifications for easy maintenance and extension
|
|
108
|
+
|
|
109
|
+
### Version 2.5
|
|
85
110
|
**Critical Bug Fixes and Performance Enhancements**
|
|
86
111
|
|
|
87
112
|
- **Fixed division by zero crashes** - Now properly handles division by zero with error messages instead of crashing
|
data/tests/README.md
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# XRPN Regression Test Framework
|
|
2
|
+
|
|
3
|
+
Comprehensive automated testing suite for XRPN to prevent regressions and ensure all commands work correctly.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
Run all tests:
|
|
8
|
+
```bash
|
|
9
|
+
ruby tests/run_tests.rb
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Run specific test category:
|
|
13
|
+
```bash
|
|
14
|
+
ruby tests/run_tests.rb tests/specs/01_basic_arithmetic.yml
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Test Framework Architecture
|
|
18
|
+
|
|
19
|
+
### Components
|
|
20
|
+
|
|
21
|
+
**run_tests.rb** - Main test runner
|
|
22
|
+
- Orchestrates test execution
|
|
23
|
+
- Parses YAML test specifications
|
|
24
|
+
- Validates outputs
|
|
25
|
+
- Reports results
|
|
26
|
+
|
|
27
|
+
**specs/** - Test specification directory
|
|
28
|
+
- YAML files defining test cases
|
|
29
|
+
- Organized by command category
|
|
30
|
+
- Easy to read and extend
|
|
31
|
+
|
|
32
|
+
### Test Specification Format
|
|
33
|
+
|
|
34
|
+
Tests are defined in YAML files in `specs/` directory:
|
|
35
|
+
|
|
36
|
+
```yaml
|
|
37
|
+
---
|
|
38
|
+
- name: "Addition: 5 + 3 = 8"
|
|
39
|
+
commands:
|
|
40
|
+
- "5"
|
|
41
|
+
- "3"
|
|
42
|
+
- "+"
|
|
43
|
+
expected:
|
|
44
|
+
x: 8
|
|
45
|
+
|
|
46
|
+
- name: "With setup"
|
|
47
|
+
setup:
|
|
48
|
+
- "deg" # Setup commands run first
|
|
49
|
+
commands:
|
|
50
|
+
- "30"
|
|
51
|
+
- "sin"
|
|
52
|
+
expected:
|
|
53
|
+
x: 0.5
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Test Fields
|
|
57
|
+
|
|
58
|
+
**name** (required) - Descriptive test name
|
|
59
|
+
|
|
60
|
+
**commands** (required) - Array of XRPN commands to execute
|
|
61
|
+
|
|
62
|
+
**setup** (optional) - Array of commands to run before test
|
|
63
|
+
|
|
64
|
+
**expected** (required) - Expected results
|
|
65
|
+
- `x`: Expected X register value (number)
|
|
66
|
+
- `tolerance`: Acceptable error margin (default: 0.0001)
|
|
67
|
+
- `contains`: String that should appear in output
|
|
68
|
+
|
|
69
|
+
**should_error** (optional) - Set to `true` if test should produce error
|
|
70
|
+
|
|
71
|
+
### Example Tests
|
|
72
|
+
|
|
73
|
+
Basic arithmetic:
|
|
74
|
+
```yaml
|
|
75
|
+
- name: "Square root: sqrt(144) = 12"
|
|
76
|
+
commands:
|
|
77
|
+
- "144"
|
|
78
|
+
- "sqrt"
|
|
79
|
+
expected:
|
|
80
|
+
x: 12
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
With setup:
|
|
84
|
+
```yaml
|
|
85
|
+
- name: "sin(30°) = 0.5"
|
|
86
|
+
setup:
|
|
87
|
+
- "deg"
|
|
88
|
+
commands:
|
|
89
|
+
- "30"
|
|
90
|
+
- "sin"
|
|
91
|
+
expected:
|
|
92
|
+
x: 0.5
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
With tolerance:
|
|
96
|
+
```yaml
|
|
97
|
+
- name: "Pi approximation"
|
|
98
|
+
commands:
|
|
99
|
+
- "pi"
|
|
100
|
+
expected:
|
|
101
|
+
x: 3.14159
|
|
102
|
+
tolerance: 0.00001
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Error handling:
|
|
106
|
+
```yaml
|
|
107
|
+
- name: "Statistics with empty registers"
|
|
108
|
+
setup:
|
|
109
|
+
- "clrg"
|
|
110
|
+
commands:
|
|
111
|
+
- "mean"
|
|
112
|
+
should_error: true
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Test Categories
|
|
116
|
+
|
|
117
|
+
Current test suites:
|
|
118
|
+
|
|
119
|
+
1. **01_basic_arithmetic.yml** - Math operations (+, -, *, /, sqrt, pow, etc.)
|
|
120
|
+
2. **02_stack_operations.yml** - Stack manipulation (enter, swap, drop, etc.)
|
|
121
|
+
3. **03_trigonometry.yml** - Trig functions (sin, cos, tan, etc.)
|
|
122
|
+
4. **04_logarithms.yml** - Log and exponential functions
|
|
123
|
+
5. **05_registers.yml** - Register operations (sto, rcl, indirect, etc.)
|
|
124
|
+
6. **06_alpha.yml** - Alpha register and string operations
|
|
125
|
+
7. **07_statistics.yml** - Statistical functions (mean, sdev, Σ+, etc.)
|
|
126
|
+
8. **08_conditionals.yml** - Conditional tests (x=0?, x>y?, etc.)
|
|
127
|
+
9. **09_base_conversion.yml** - Number base conversions
|
|
128
|
+
10. **10_flags.yml** - Flag operations (sf, cf, fs?, fc?)
|
|
129
|
+
|
|
130
|
+
## Adding New Tests
|
|
131
|
+
|
|
132
|
+
### Create New Test Category
|
|
133
|
+
|
|
134
|
+
1. Create new YAML file in `tests/specs/`:
|
|
135
|
+
```bash
|
|
136
|
+
vi tests/specs/11_my_category.yml
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
2. Add test cases:
|
|
140
|
+
```yaml
|
|
141
|
+
---
|
|
142
|
+
# My Category Tests
|
|
143
|
+
- name: "My first test"
|
|
144
|
+
commands:
|
|
145
|
+
- "cmd1"
|
|
146
|
+
- "cmd2"
|
|
147
|
+
expected:
|
|
148
|
+
x: 42
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
3. Run tests:
|
|
152
|
+
```bash
|
|
153
|
+
ruby tests/run_tests.rb
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Add Test to Existing Category
|
|
157
|
+
|
|
158
|
+
Edit the appropriate YAML file in `tests/specs/` and add your test case following the format above.
|
|
159
|
+
|
|
160
|
+
## How It Works
|
|
161
|
+
|
|
162
|
+
1. **Test Execution**
|
|
163
|
+
- Reads YAML test specifications
|
|
164
|
+
- Builds command strings with setup + commands + stdout + off
|
|
165
|
+
- Pipes commands to xrpn: `echo "cmd1,cmd2,stdout,off" | xrpn`
|
|
166
|
+
- Captures first line of output (the stdout result)
|
|
167
|
+
|
|
168
|
+
2. **Validation**
|
|
169
|
+
- Parses numeric output from stdout
|
|
170
|
+
- Compares against expected value within tolerance
|
|
171
|
+
- Reports pass/fail for each test
|
|
172
|
+
|
|
173
|
+
3. **Output**
|
|
174
|
+
- Clear pass (✓) / fail (✗) indicators
|
|
175
|
+
- Expected vs actual values for failures
|
|
176
|
+
- Summary with total/passed/failed counts
|
|
177
|
+
|
|
178
|
+
## Best Practices
|
|
179
|
+
|
|
180
|
+
### Test Naming
|
|
181
|
+
- Be descriptive: "sin(30°) = 0.5" not "test sin"
|
|
182
|
+
- Include operation and expected result
|
|
183
|
+
- Use standard math notation
|
|
184
|
+
|
|
185
|
+
### Test Organization
|
|
186
|
+
- Group related commands in same file
|
|
187
|
+
- Use setup for common initialization
|
|
188
|
+
- One test per specific behavior
|
|
189
|
+
|
|
190
|
+
### Tolerances
|
|
191
|
+
- Default 0.0001 works for most cases
|
|
192
|
+
- Increase for transcendental functions: `tolerance: 0.00001`
|
|
193
|
+
- Use exact match for integers (default tolerance fine)
|
|
194
|
+
|
|
195
|
+
### Edge Cases
|
|
196
|
+
- Test boundary conditions
|
|
197
|
+
- Test error handling with `should_error: true`
|
|
198
|
+
- Test indirect addressing
|
|
199
|
+
- Test register overflow/underflow
|
|
200
|
+
|
|
201
|
+
## Extending the Framework
|
|
202
|
+
|
|
203
|
+
### Custom Validators
|
|
204
|
+
|
|
205
|
+
Edit `run_tests.rb` to add custom validation logic:
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
def validate_output(output, expected, test)
|
|
209
|
+
# Add custom validation here
|
|
210
|
+
end
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Output Formats
|
|
214
|
+
|
|
215
|
+
Currently validates X register via stdout.
|
|
216
|
+
Future: Add alpha, register, flag validation.
|
|
217
|
+
|
|
218
|
+
### Continuous Integration
|
|
219
|
+
|
|
220
|
+
Add to CI pipeline:
|
|
221
|
+
```bash
|
|
222
|
+
#!/bin/bash
|
|
223
|
+
cd xrpn
|
|
224
|
+
ruby tests/run_tests.rb
|
|
225
|
+
exit $?
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Troubleshooting
|
|
229
|
+
|
|
230
|
+
**Tests hang/timeout:**
|
|
231
|
+
- Ensure all test programs include implicit "off" (added automatically)
|
|
232
|
+
- Check for infinite loops in test commands
|
|
233
|
+
|
|
234
|
+
**Numeric comparison failures:**
|
|
235
|
+
- Increase tolerance if needed
|
|
236
|
+
- Check European vs US decimal format (handled automatically)
|
|
237
|
+
|
|
238
|
+
**Command not found:**
|
|
239
|
+
- Verify command exists in `/xcmd/` or `/xlib/`
|
|
240
|
+
- Check command spelling in YAML
|
|
241
|
+
|
|
242
|
+
## Contributing Tests
|
|
243
|
+
|
|
244
|
+
When adding new XRPN features:
|
|
245
|
+
|
|
246
|
+
1. Write tests FIRST (TDD approach)
|
|
247
|
+
2. Run tests to see them fail
|
|
248
|
+
3. Implement feature
|
|
249
|
+
4. Run tests to see them pass
|
|
250
|
+
5. Commit both feature and tests together
|
|
251
|
+
|
|
252
|
+
This ensures:
|
|
253
|
+
- Features work as intended
|
|
254
|
+
- Future changes don't break existing functionality
|
|
255
|
+
- Documentation via executable examples
|
|
256
|
+
|
|
257
|
+
## License
|
|
258
|
+
|
|
259
|
+
Same as XRPN - Public Domain (Unlicense)
|
data/tests/run_tests.rb
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# encoding: utf-8
|
|
3
|
+
#
|
|
4
|
+
# XRPN Regression Test Framework
|
|
5
|
+
# Tests all xrpn commands to prevent regressions
|
|
6
|
+
# Usage: ruby tests/run_tests.rb [test_file.yml]
|
|
7
|
+
|
|
8
|
+
require 'yaml'
|
|
9
|
+
require 'json'
|
|
10
|
+
require 'fileutils'
|
|
11
|
+
|
|
12
|
+
class XRPNTestRunner
|
|
13
|
+
attr_reader :passed, :failed, :errors
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@passed = 0
|
|
17
|
+
@failed = 0
|
|
18
|
+
@errors = 0
|
|
19
|
+
@test_results = []
|
|
20
|
+
@xrpn_bin = File.expand_path('../bin/xrpn', __dir__)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def run_all_tests
|
|
24
|
+
test_dir = File.dirname(__FILE__)
|
|
25
|
+
test_files = Dir["#{test_dir}/specs/*.yml"].sort
|
|
26
|
+
|
|
27
|
+
if test_files.empty?
|
|
28
|
+
puts "No test files found in #{test_dir}/specs/"
|
|
29
|
+
puts "Creating test specification directory..."
|
|
30
|
+
FileUtils.mkdir_p("#{test_dir}/specs")
|
|
31
|
+
return
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
puts "=" * 60
|
|
35
|
+
puts "XRPN Regression Test Suite"
|
|
36
|
+
puts "=" * 60
|
|
37
|
+
puts
|
|
38
|
+
|
|
39
|
+
test_files.each do |file|
|
|
40
|
+
run_test_file(file)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
print_summary
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def run_test_file(file)
|
|
47
|
+
tests = YAML.load_file(file)
|
|
48
|
+
category = File.basename(file, '.yml')
|
|
49
|
+
|
|
50
|
+
puts "Testing: #{category}"
|
|
51
|
+
puts "-" * 60
|
|
52
|
+
|
|
53
|
+
tests.each do |test|
|
|
54
|
+
run_single_test(test, category)
|
|
55
|
+
end
|
|
56
|
+
puts
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def run_single_test(test, category)
|
|
60
|
+
name = test['name']
|
|
61
|
+
commands = test['commands']
|
|
62
|
+
expected = test['expected'] || {}
|
|
63
|
+
setup = test['setup'] || []
|
|
64
|
+
|
|
65
|
+
# Build XRPN program as comma-separated commands
|
|
66
|
+
program = build_program(setup, commands, expected)
|
|
67
|
+
|
|
68
|
+
# Write to temp file to avoid shell escaping issues
|
|
69
|
+
temp_file = "/tmp/xrpn_test_#{Process.pid}_#{rand(10000)}.txt"
|
|
70
|
+
File.write(temp_file, program)
|
|
71
|
+
|
|
72
|
+
# Run xrpn by piping commands and capture output
|
|
73
|
+
output = `cat #{temp_file} | #{@xrpn_bin} 2>&1`
|
|
74
|
+
exit_code = $?.exitstatus
|
|
75
|
+
|
|
76
|
+
# Clean up
|
|
77
|
+
File.delete(temp_file) if File.exist?(temp_file)
|
|
78
|
+
|
|
79
|
+
# Parse output and validate
|
|
80
|
+
result = validate_output(output, expected, test)
|
|
81
|
+
|
|
82
|
+
if result[:passed]
|
|
83
|
+
@passed += 1
|
|
84
|
+
print " ✓ #{name}"
|
|
85
|
+
puts " (#{category})"
|
|
86
|
+
else
|
|
87
|
+
@failed += 1
|
|
88
|
+
print " ✗ #{name}"
|
|
89
|
+
puts " (#{category})"
|
|
90
|
+
puts " Expected: #{result[:expected]}"
|
|
91
|
+
puts " Got: #{result[:actual]}"
|
|
92
|
+
puts " Output: #{output.strip}" if test['verbose']
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
rescue => e
|
|
96
|
+
@errors += 1
|
|
97
|
+
puts " ✗ #{name} - ERROR: #{e.message}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def build_program(setup, commands, expected)
|
|
101
|
+
lines = []
|
|
102
|
+
|
|
103
|
+
# Setup phase
|
|
104
|
+
setup.each { |cmd| lines << cmd }
|
|
105
|
+
|
|
106
|
+
# Test commands
|
|
107
|
+
commands.each { |cmd| lines << cmd }
|
|
108
|
+
|
|
109
|
+
# If testing alpha, move alpha to X register before stdout
|
|
110
|
+
lines << "asto x" if expected['alpha']
|
|
111
|
+
|
|
112
|
+
# Output results for validation using stdout
|
|
113
|
+
lines << "stdout" # Output X to stdout for capture
|
|
114
|
+
lines << "off" # Exit cleanly
|
|
115
|
+
|
|
116
|
+
lines.join(",") # Join with commas
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def validate_output(output, expected, test)
|
|
120
|
+
result = { passed: true, expected: {}, actual: {} }
|
|
121
|
+
|
|
122
|
+
# Check for error conditions
|
|
123
|
+
if test['should_error']
|
|
124
|
+
if output.include?("Error") || output.include?("ERROR") || output.include?("error")
|
|
125
|
+
return result
|
|
126
|
+
else
|
|
127
|
+
result[:passed] = false
|
|
128
|
+
result[:expected] = "Error condition"
|
|
129
|
+
result[:actual] = "No error"
|
|
130
|
+
return result
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Parse X register from stdout output (just a number on first line)
|
|
135
|
+
if expected['x']
|
|
136
|
+
# Get first line and convert European decimal format to standard
|
|
137
|
+
first_line = output.split("\n").first.to_s.strip
|
|
138
|
+
clean_output = first_line.gsub(',', '.')
|
|
139
|
+
actual_x = clean_output.to_f rescue nil
|
|
140
|
+
expected_x = expected['x'].to_f
|
|
141
|
+
|
|
142
|
+
tolerance = expected['tolerance'] || 0.0001
|
|
143
|
+
if actual_x.nil? || (actual_x - expected_x).abs > tolerance
|
|
144
|
+
result[:passed] = false
|
|
145
|
+
result[:expected]['x'] = expected_x
|
|
146
|
+
result[:actual]['x'] = actual_x
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Parse Alpha register from stdout output (string on first line)
|
|
151
|
+
if expected['alpha']
|
|
152
|
+
first_line = output.split("\n").first.to_s.strip
|
|
153
|
+
actual_alpha = first_line
|
|
154
|
+
expected_alpha = expected['alpha']
|
|
155
|
+
|
|
156
|
+
if actual_alpha != expected_alpha
|
|
157
|
+
result[:passed] = false
|
|
158
|
+
result[:expected]['alpha'] = expected_alpha
|
|
159
|
+
result[:actual]['alpha'] = actual_alpha
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Parse string in X register (for commands like dechex that put strings in X)
|
|
164
|
+
if expected['x_string']
|
|
165
|
+
first_line = output.split("\n").first.to_s.strip
|
|
166
|
+
actual_string = first_line
|
|
167
|
+
expected_string = expected['x_string']
|
|
168
|
+
|
|
169
|
+
if actual_string != expected_string
|
|
170
|
+
result[:passed] = false
|
|
171
|
+
result[:expected]['x_string'] = expected_string
|
|
172
|
+
result[:actual]['x_string'] = actual_string
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Check for specific output patterns
|
|
177
|
+
if expected['contains']
|
|
178
|
+
unless output.include?(expected['contains'])
|
|
179
|
+
result[:passed] = false
|
|
180
|
+
result[:expected]['output'] = "Contains '#{expected['contains']}'"
|
|
181
|
+
result[:actual]['output'] = "Not found"
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
result
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def print_summary
|
|
189
|
+
total = @passed + @failed + @errors
|
|
190
|
+
|
|
191
|
+
puts "=" * 60
|
|
192
|
+
puts "Test Results Summary"
|
|
193
|
+
puts "=" * 60
|
|
194
|
+
puts "Total tests: #{total}"
|
|
195
|
+
puts "Passed: #{@passed} ✓"
|
|
196
|
+
puts "Failed: #{@failed} ✗" if @failed > 0
|
|
197
|
+
puts "Errors: #{@errors} ✗" if @errors > 0
|
|
198
|
+
puts
|
|
199
|
+
|
|
200
|
+
if @failed == 0 && @errors == 0
|
|
201
|
+
puts "SUCCESS: All tests passed!"
|
|
202
|
+
exit 0
|
|
203
|
+
else
|
|
204
|
+
puts "FAILURE: #{@failed + @errors} test(s) failed"
|
|
205
|
+
exit 1
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Run tests
|
|
211
|
+
if __FILE__ == $0
|
|
212
|
+
runner = XRPNTestRunner.new
|
|
213
|
+
|
|
214
|
+
if ARGV[0]
|
|
215
|
+
runner.run_test_file(ARGV[0])
|
|
216
|
+
runner.print_summary
|
|
217
|
+
else
|
|
218
|
+
runner.run_all_tests
|
|
219
|
+
end
|
|
220
|
+
end
|
data/xcmd/rawinfo
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class XRPN
|
|
2
|
+
# Display information about HP-41 RAW file
|
|
3
|
+
# Usage: "filename.raw" then RAWINFO
|
|
4
|
+
def rawinfo
|
|
5
|
+
filename = @a
|
|
6
|
+
if filename.empty?
|
|
7
|
+
puts "Error: No filename in Alpha register"
|
|
8
|
+
puts "Usage: Put RAW filename in Alpha, then run RAWINFO"
|
|
9
|
+
return
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Check file exists
|
|
13
|
+
unless File.exist?(filename)
|
|
14
|
+
puts "Error: File not found: #{filename}"
|
|
15
|
+
return
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
raw_info(filename)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# vim:ft=ruby:
|
data/xlib/_xrpn_version
CHANGED
data/xlib/raw_info
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# HP-41 RAW File Information Viewer
|
|
2
|
+
# Displays labels, strings, and basic info from HP-41 RAW files
|
|
3
|
+
|
|
4
|
+
def raw_info(filename)
|
|
5
|
+
begin
|
|
6
|
+
data = File.binread(filename)
|
|
7
|
+
|
|
8
|
+
puts "=" * 60
|
|
9
|
+
puts "HP-41 RAW File: #{File.basename(filename)}"
|
|
10
|
+
puts "File size: #{data.length} bytes"
|
|
11
|
+
puts "=" * 60
|
|
12
|
+
puts
|
|
13
|
+
|
|
14
|
+
# Extract labels
|
|
15
|
+
labels = []
|
|
16
|
+
strings = []
|
|
17
|
+
pos = 0
|
|
18
|
+
|
|
19
|
+
while pos < data.length
|
|
20
|
+
byte = data[pos].ord
|
|
21
|
+
|
|
22
|
+
# Global label (C0 00 Fx yy LABELNAME)
|
|
23
|
+
if byte == 0xC0 && pos + 3 < data.length && data[pos + 1].ord == 0x00
|
|
24
|
+
label_type = data[pos + 2].ord
|
|
25
|
+
pos += 4
|
|
26
|
+
|
|
27
|
+
# Extract label name
|
|
28
|
+
label = ""
|
|
29
|
+
while pos < data.length && data[pos].ord >= 0x20 && data[pos].ord < 0x7F
|
|
30
|
+
label << data[pos]
|
|
31
|
+
pos += 1
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
type_str = case label_type
|
|
35
|
+
when 0xF4 then "Alpha"
|
|
36
|
+
when 0xF5 then "Numeric"
|
|
37
|
+
else "Unknown"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
labels << " LBL \"#{label}\" (#{type_str})"
|
|
41
|
+
next
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Local label (F6 00 LABELNAME)
|
|
45
|
+
if byte == 0xF6 && pos + 1 < data.length && data[pos + 1].ord == 0x00
|
|
46
|
+
pos += 2
|
|
47
|
+
|
|
48
|
+
# Extract label name
|
|
49
|
+
label = ""
|
|
50
|
+
while pos < data.length && data[pos].ord >= 0x20 && data[pos].ord < 0x7F
|
|
51
|
+
label << data[pos]
|
|
52
|
+
pos += 1
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
labels << " LBL \"#{label}\" (Local)"
|
|
56
|
+
next
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Text strings (multiple formats)
|
|
60
|
+
# Format 1: 7F 20 TEXT
|
|
61
|
+
if byte == 0x7F && pos + 1 < data.length && data[pos + 1].ord == 0x20
|
|
62
|
+
pos += 2
|
|
63
|
+
text = ""
|
|
64
|
+
while pos < data.length && data[pos].ord >= 0x20 && data[pos].ord < 0x7F
|
|
65
|
+
text << data[pos]
|
|
66
|
+
pos += 1
|
|
67
|
+
end
|
|
68
|
+
strings << " \"#{text}\"" unless text.empty?
|
|
69
|
+
next
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Format 2: FD TEXT (string literals in prompts)
|
|
73
|
+
if byte == 0xFD && pos + 1 < data.length
|
|
74
|
+
pos += 1
|
|
75
|
+
text = ""
|
|
76
|
+
while pos < data.length && data[pos].ord >= 0x20 && data[pos].ord < 0x7F
|
|
77
|
+
text << data[pos]
|
|
78
|
+
pos += 1
|
|
79
|
+
end
|
|
80
|
+
strings << " \"#{text}\"" unless text.empty?
|
|
81
|
+
next
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Format 3: F8 TEXT (another string format)
|
|
85
|
+
if byte == 0xF8 && pos + 1 < data.length
|
|
86
|
+
pos += 1
|
|
87
|
+
text = ""
|
|
88
|
+
while pos < data.length && data[pos].ord >= 0x20 && data[pos].ord < 0x7F
|
|
89
|
+
text << data[pos]
|
|
90
|
+
pos += 1
|
|
91
|
+
end
|
|
92
|
+
strings << " \"#{text}\"" unless text.empty?
|
|
93
|
+
next
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Format 4: F3 TEXT (yet another string format)
|
|
97
|
+
if byte == 0xF3 && pos + 1 < data.length
|
|
98
|
+
pos += 1
|
|
99
|
+
text = ""
|
|
100
|
+
while pos < data.length && data[pos].ord >= 0x20 && data[pos].ord < 0x7F
|
|
101
|
+
text << data[pos]
|
|
102
|
+
pos += 1
|
|
103
|
+
end
|
|
104
|
+
strings << " \"#{text}\"" unless text.empty?
|
|
105
|
+
next
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Format 5: FB TEXT (text string marker)
|
|
109
|
+
if byte == 0xFB && pos + 1 < data.length
|
|
110
|
+
pos += 1
|
|
111
|
+
text = ""
|
|
112
|
+
while pos < data.length && data[pos].ord >= 0x20 && data[pos].ord < 0x7F
|
|
113
|
+
text << data[pos]
|
|
114
|
+
pos += 1
|
|
115
|
+
end
|
|
116
|
+
strings << " \"#{text}\"" unless text.empty?
|
|
117
|
+
next
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
pos += 1
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Display findings
|
|
124
|
+
puts "Labels found (#{labels.length}):"
|
|
125
|
+
if labels.empty?
|
|
126
|
+
puts " (none)"
|
|
127
|
+
else
|
|
128
|
+
labels.each { |l| puts l }
|
|
129
|
+
end
|
|
130
|
+
puts
|
|
131
|
+
|
|
132
|
+
puts "Text strings found (#{strings.length}):"
|
|
133
|
+
if strings.empty?
|
|
134
|
+
puts " (none)"
|
|
135
|
+
else
|
|
136
|
+
strings.each { |s| puts s }
|
|
137
|
+
end
|
|
138
|
+
puts
|
|
139
|
+
|
|
140
|
+
puts "Hex dump (first 256 bytes):"
|
|
141
|
+
puts "-" * 60
|
|
142
|
+
system("hexdump -C '#{filename}' | head -20")
|
|
143
|
+
puts
|
|
144
|
+
|
|
145
|
+
puts "Note: Full RAW to XRPN conversion requires comprehensive"
|
|
146
|
+
puts "HP-41 bytecode documentation. This viewer extracts readable"
|
|
147
|
+
puts "labels and strings for informational purposes."
|
|
148
|
+
puts "=" * 60
|
|
149
|
+
|
|
150
|
+
rescue => e
|
|
151
|
+
puts "Error reading RAW file: #{e.message}"
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# vim:ft=ruby:
|
data/xrpn.gemspec
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Gem::Specification.new do |s|
|
|
2
2
|
s.name = 'xrpn'
|
|
3
|
-
s.version = '2.
|
|
3
|
+
s.version = '2.6'
|
|
4
4
|
s.licenses = ['Unlicense']
|
|
5
5
|
s.summary = "XRPN - The eXtended RPN (Reverse Polish Notation) programming language"
|
|
6
|
-
s.description = "A full programming language and environment extending the features of the venerable HP calculator programmable calculators. With XRPN you can accomplish a wide range of modern programming tasks as well as running existing HP-41 FOCAL programs directly. XRPN gives modern life to tens of thousands of old HP calculator programs and opens the possibilities to many, many more. New in 2.
|
|
6
|
+
s.description = "A full programming language and environment extending the features of the venerable HP calculator programmable calculators. With XRPN you can accomplish a wide range of modern programming tasks as well as running existing HP-41 FOCAL programs directly. XRPN gives modern life to tens of thousands of old HP calculator programs and opens the possibilities to many, many more. New in 2.6: Comprehensive regression test framework with 58 automated tests and HP-41 RAW file viewer for inspecting thousands of legacy HP-41 programs."
|
|
7
7
|
|
|
8
8
|
s.authors = ["Geir Isene"]
|
|
9
9
|
s.email = 'g@isene.com'
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: xrpn
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: '2.
|
|
4
|
+
version: '2.6'
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Geir Isene
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-
|
|
11
|
+
date: 2025-10-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: tty-prompt
|
|
@@ -28,9 +28,9 @@ description: 'A full programming language and environment extending the features
|
|
|
28
28
|
the venerable HP calculator programmable calculators. With XRPN you can accomplish
|
|
29
29
|
a wide range of modern programming tasks as well as running existing HP-41 FOCAL
|
|
30
30
|
programs directly. XRPN gives modern life to tens of thousands of old HP calculator
|
|
31
|
-
programs and opens the possibilities to many, many more. New in 2.
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
programs and opens the possibilities to many, many more. New in 2.6: Comprehensive
|
|
32
|
+
regression test framework with 58 automated tests and HP-41 RAW file viewer for
|
|
33
|
+
inspecting thousands of legacy HP-41 programs.'
|
|
34
34
|
email: g@isene.com
|
|
35
35
|
executables:
|
|
36
36
|
- xrpn
|
|
@@ -40,7 +40,9 @@ files:
|
|
|
40
40
|
- README.md
|
|
41
41
|
- bin/xrpn
|
|
42
42
|
- conf
|
|
43
|
+
- tests/README.md
|
|
43
44
|
- tests/final_test.xrpn
|
|
45
|
+
- tests/run_tests.rb
|
|
44
46
|
- tests/test_bugs.xrpn
|
|
45
47
|
- tests/test_bugs_simple.xrpn
|
|
46
48
|
- tests/test_final.xrpn
|
|
@@ -226,6 +228,7 @@ files:
|
|
|
226
228
|
- xcmd/r_p
|
|
227
229
|
- xcmd/rad
|
|
228
230
|
- xcmd/rand
|
|
231
|
+
- xcmd/rawinfo
|
|
229
232
|
- xcmd/rcl
|
|
230
233
|
- xcmd/rclaf
|
|
231
234
|
- xcmd/rclflag
|
|
@@ -334,6 +337,7 @@ files:
|
|
|
334
337
|
- xlib/locate_prg
|
|
335
338
|
- xlib/numeric
|
|
336
339
|
- xlib/numformat
|
|
340
|
+
- xlib/raw_info
|
|
337
341
|
- xlib/read_state
|
|
338
342
|
- xlib/read_xcmd
|
|
339
343
|
- xlib/rxcmd
|