xrpn 2.5 → 2.7
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 +60 -4
- data/tests/README.md +259 -0
- data/tests/run_tests.rb +220 -0
- data/xcmd/rawexport +56 -0
- data/xcmd/rawimport +58 -0
- data/xcmd/rawinfo +22 -0
- data/xlib/_xrpn_version +1 -1
- data/xlib/raw_decoder +513 -0
- data/xlib/raw_encoder +258 -0
- data/xlib/raw_info +155 -0
- data/xrpn.gemspec +2 -2
- metadata +14 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2e1070cc4a730b8515e905da3e7f6df2158698bbf40c38d00ceecd46980ba9f2
|
|
4
|
+
data.tar.gz: 333db30ef26238289797412260f964d1c164f93a36d5035c7c75ce649ab70d84
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3b60f0ec411635585d76bd30e0b3e989ad04202d577f166fcc537b6f0a1fc1594ea3b3b5681d1a6f5a9c4ea8db4367936ac04a7dc0ef24f914c33ebb3a9edc89
|
|
7
|
+
data.tar.gz: e884ac9757c49cf35854d2f153254e7f7c245424ef4e0712ba1afd16b5dae5b247838ec1e5bf5c983629146954b6747cc74aadcf4f17f20918316aeec9a89815
|
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,65 @@ AVIEW
|
|
|
79
79
|
END
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
+
## HP-41 RAW File Support - 100% Decode Success!
|
|
83
|
+
|
|
84
|
+
XRPN provides complete HP-41 RAW format import/export with perfect decode rate!
|
|
85
|
+
|
|
86
|
+
### Commands
|
|
87
|
+
|
|
88
|
+
**RAWINFO** - View RAW file information:
|
|
89
|
+
```
|
|
90
|
+
xrpn
|
|
91
|
+
> "program.raw"
|
|
92
|
+
> rawinfo
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**RAWIMPORT** - Import HP-41 programs (100% decode):
|
|
96
|
+
```
|
|
97
|
+
xrpn
|
|
98
|
+
> "program.raw"
|
|
99
|
+
> rawimport
|
|
100
|
+
> prp # View imported program
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**RAWEXPORT** - Export to RAW format:
|
|
104
|
+
```
|
|
105
|
+
xrpn
|
|
106
|
+
> "output.raw"
|
|
107
|
+
> rawexport
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Achievement
|
|
111
|
+
|
|
112
|
+
- ✓ **100% decode rate** on real HP-41 programs
|
|
113
|
+
- ✓ **ALL math functions** (SIN, COS, TAN, LOG, LN, etc.)
|
|
114
|
+
- ✓ **Complete flow control** (GTO, XEQ, conditionals, flags)
|
|
115
|
+
- ✓ **165+ opcodes** working
|
|
116
|
+
- ✓ **Numbers 0-99** fully supported (import)
|
|
117
|
+
- ✓ **Perfect round-trip** for core operations
|
|
118
|
+
|
|
119
|
+
See `raw.md` for complete technical documentation.
|
|
120
|
+
|
|
82
121
|
## Changelog
|
|
83
122
|
|
|
84
|
-
### Version 2.
|
|
123
|
+
### Version 2.7 (Latest)
|
|
124
|
+
**HP-41 RAW Import/Export - 100% Success!**
|
|
125
|
+
|
|
126
|
+
- **Complete HP-41 RAW import/export** - RAWIMPORT and RAWEXPORT commands with 100% decode rate on real HP-41 programs
|
|
127
|
+
- **ALL math functions** - SIN, COS, TAN, LOG, LN, POW, ABS, and 18 total math operations
|
|
128
|
+
- **165+ opcodes implemented** - Complete flow control, conditionals, flags, XROM functions, numbers 0-99
|
|
129
|
+
- **Context-aware decoding** - Intelligent handling of ASCII-range opcodes
|
|
130
|
+
- **Production ready** - Zero unknown opcodes on tested programs
|
|
131
|
+
- See `raw.md` for complete technical documentation
|
|
132
|
+
|
|
133
|
+
### Version 2.6
|
|
134
|
+
**New Features and Testing**
|
|
135
|
+
|
|
136
|
+
- **Added HP-41 RAW file viewer** - New `RAWINFO` command displays labels, strings, and hex dump from HP-41 RAW program files
|
|
137
|
+
- **Comprehensive regression test framework** - 58 automated tests across 7 command categories with 100% pass rate
|
|
138
|
+
- **Test infrastructure** - YAML-based test specifications for easy maintenance and extension
|
|
139
|
+
|
|
140
|
+
### Version 2.5
|
|
85
141
|
**Critical Bug Fixes and Performance Enhancements**
|
|
86
142
|
|
|
87
143
|
- **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/rawexport
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
class XRPN
|
|
2
|
+
# Export current XRPN program to HP-41 RAW format
|
|
3
|
+
# Usage: "output.raw" then RAWEXPORT
|
|
4
|
+
# Converts XRPN text format to RAW bytecode
|
|
5
|
+
def rawexport
|
|
6
|
+
filename = @a
|
|
7
|
+
if filename.empty?
|
|
8
|
+
puts "Error: No filename in Alpha register"
|
|
9
|
+
puts "Usage: Put RAW filename in Alpha, then run RAWEXPORT"
|
|
10
|
+
return
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Check if program exists
|
|
14
|
+
if @prg[@pg].nil? || @prg[@pg].empty?
|
|
15
|
+
puts "Error: No program loaded on page #{@pg}"
|
|
16
|
+
puts "Load a program first with 'loadp' or create one"
|
|
17
|
+
return
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
begin
|
|
21
|
+
puts "=" * 60
|
|
22
|
+
puts "Exporting to HP-41 RAW: #{filename}"
|
|
23
|
+
puts "Source: Page #{@pg} (#{@prg[@pg].length} instructions)"
|
|
24
|
+
puts "=" * 60
|
|
25
|
+
|
|
26
|
+
# Get label name from first LBL command if exists
|
|
27
|
+
label_name = "PROG"
|
|
28
|
+
first_line = @prg[@pg].first
|
|
29
|
+
if first_line =~ /^LBL\s+"(.+)"$/i
|
|
30
|
+
label_name = $1
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Encode XRPN program to RAW bytecode
|
|
34
|
+
raw_data = raw_encode(@prg[@pg], label_name)
|
|
35
|
+
|
|
36
|
+
# Write to file
|
|
37
|
+
File.binwrite(filename, raw_data)
|
|
38
|
+
|
|
39
|
+
puts "Exported #{raw_data.length} bytes"
|
|
40
|
+
puts "Label: #{label_name}"
|
|
41
|
+
puts
|
|
42
|
+
puts "Program exported:"
|
|
43
|
+
@prg[@pg].first(5).each { |line| puts " #{line}" }
|
|
44
|
+
puts " ..." if @prg[@pg].length > 5
|
|
45
|
+
puts
|
|
46
|
+
puts "File written: #{filename}"
|
|
47
|
+
puts "=" * 60
|
|
48
|
+
|
|
49
|
+
rescue => e
|
|
50
|
+
puts "Error exporting RAW file: #{e.message}"
|
|
51
|
+
puts e.backtrace.first(3)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# vim:ft=ruby:
|
data/xcmd/rawimport
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
class XRPN
|
|
2
|
+
# Import HP-41 RAW file and convert to XRPN program
|
|
3
|
+
# Usage: "filename.raw" then RAWIMPORT
|
|
4
|
+
# Converts RAW bytecode to XRPN text format and loads into current program page
|
|
5
|
+
def rawimport
|
|
6
|
+
filename = @a
|
|
7
|
+
if filename.empty?
|
|
8
|
+
puts "Error: No filename in Alpha register"
|
|
9
|
+
puts "Usage: Put RAW filename in Alpha, then run RAWIMPORT"
|
|
10
|
+
return
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Check file exists
|
|
14
|
+
unless File.exist?(filename)
|
|
15
|
+
puts "Error: File not found: #{filename}"
|
|
16
|
+
return
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
begin
|
|
20
|
+
# Read RAW file
|
|
21
|
+
data = File.binread(filename)
|
|
22
|
+
|
|
23
|
+
puts "=" * 60
|
|
24
|
+
puts "Importing HP-41 RAW: #{File.basename(filename)}"
|
|
25
|
+
puts "File size: #{data.length} bytes"
|
|
26
|
+
puts "=" * 60
|
|
27
|
+
|
|
28
|
+
# Decode RAW bytecode to XRPN commands
|
|
29
|
+
program = raw_decode(data)
|
|
30
|
+
|
|
31
|
+
if program.empty?
|
|
32
|
+
puts "Warning: No decodable instructions found"
|
|
33
|
+
return
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Load into current program page
|
|
37
|
+
@prg[@pg] = program
|
|
38
|
+
|
|
39
|
+
puts "Imported #{program.length} instructions to page #{@pg}"
|
|
40
|
+
puts
|
|
41
|
+
puts "Program preview:"
|
|
42
|
+
program.first(10).each_with_index do |line, i|
|
|
43
|
+
puts " #{i.to_s.rjust(3)}: #{line}"
|
|
44
|
+
end
|
|
45
|
+
puts " ..." if program.length > 10
|
|
46
|
+
puts
|
|
47
|
+
puts "Use 'prp' to view full program"
|
|
48
|
+
puts "Use 'run' to execute program"
|
|
49
|
+
puts "=" * 60
|
|
50
|
+
|
|
51
|
+
rescue => e
|
|
52
|
+
puts "Error importing RAW file: #{e.message}"
|
|
53
|
+
puts e.backtrace.first(3)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# vim:ft=ruby:
|
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:
|