Skip to content

Commit 207ed4b

Browse files
committed
test(ruby): runtime test suite and fixture enablement
packages/@jsii/ruby-runtime-test (new): 14 compliance spec files tagged case-for-case against tools/jsii-compliance/suite.ts, plus unit specs for the riskiest machinery (autoload, callbacks, serializer, SAM coercion, concurrency, RBS validation with a negative control). generate.sh regenerates the jsii-calc bindings unconditionally so specs can never run against stale generated code. Enablement edits outside the new package: targets.ruby declarations on the four jsii-calc fixture assemblies, matching ruby naming expectations in @jsii/kernel's kernel.test.ts, and a bash shebang on update-all.sh.
1 parent d62d502 commit 207ed4b

42 files changed

Lines changed: 3934 additions & 1 deletion

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/@jsii/kernel/src/kernel.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,10 @@ defineTest(
410410
module: 'jsii_calc',
411411
classifiers: ['Test :: Classifier :: Is Dummy'],
412412
},
413+
ruby: {
414+
gem: 'jsii-calc',
415+
module: 'JsiiCalc',
416+
},
413417
});
414418
expect(sandbox.naming({ assembly: '@scope/jsii-calc-lib' }).naming).toEqual(
415419
{
@@ -435,6 +439,10 @@ defineTest(
435439
distName: 'scope.jsii-calc-lib',
436440
module: 'scope.jsii_calc_lib',
437441
},
442+
ruby: {
443+
gem: 'scope-jsii-calc-lib',
444+
module: 'Scope::JsiiCalcLib',
445+
},
438446
},
439447
);
440448
},
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
lib/
2+
compliance-report.json
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
source 'https://rubygems.org'
2+
3+
gem 'jsii-ruby-runtime', path: '../ruby-runtime'
4+
gem 'rspec', '~> 3.12'
5+
# Used by spec/unit/rbs_spec.rb to validate the generator's emitted `sig/*.rbs`.
6+
gem 'rbs', '~> 3.4'
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
PATH
2+
remote: ../ruby-runtime
3+
specs:
4+
jsii-ruby-runtime (0.0.0)
5+
base64 (~> 0.2)
6+
7+
GEM
8+
remote: https://rubygems.org/
9+
specs:
10+
base64 (0.3.0)
11+
diff-lcs (1.6.2)
12+
logger (1.7.0)
13+
rbs (3.10.4)
14+
logger
15+
tsort
16+
rspec (3.13.2)
17+
rspec-core (~> 3.13.0)
18+
rspec-expectations (~> 3.13.0)
19+
rspec-mocks (~> 3.13.0)
20+
rspec-core (3.13.6)
21+
rspec-support (~> 3.13.0)
22+
rspec-expectations (3.13.5)
23+
diff-lcs (>= 1.2.0, < 2.0)
24+
rspec-support (~> 3.13.0)
25+
rspec-mocks (3.13.8)
26+
diff-lcs (>= 1.2.0, < 2.0)
27+
rspec-support (~> 3.13.0)
28+
rspec-support (3.13.7)
29+
tsort (0.2.0)
30+
31+
PLATFORMS
32+
ruby
33+
x86_64-linux
34+
35+
DEPENDENCIES
36+
jsii-ruby-runtime!
37+
rbs (~> 3.4)
38+
rspec (~> 3.12)
39+
40+
CHECKSUMS
41+
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
42+
diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
43+
jsii-ruby-runtime (0.0.0)
44+
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
45+
rbs (3.10.4) sha256=b17d7c4be4bb31a11a3b529830f0aa206a807ca42f2e7921a3027dfc6b7e5ce8
46+
rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
47+
rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
48+
rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
49+
rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47
50+
rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c
51+
tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
52+
53+
BUNDLED WITH
54+
2.6.9
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#!/usr/bin/env bash
2+
# Generate the Ruby bindings for the jsii-calc fixture assemblies into ./lib,
3+
# where spec/spec_helper.rb expects to find them. Invoked by `yarn build`.
4+
#
5+
# `yarn test` runs this every time, unconditionally — do NOT reintroduce a
6+
# `test -d ./lib || ...` guard. lib/ is gitignored generated output; when it is
7+
# present but stale (built by an older pacmak/rosetta) such a guard silently
8+
# runs the specs against months-old bindings and reports phantom failures. This
9+
# script is only a few seconds, so always regenerating is the safe default.
10+
set -euo pipefail
11+
12+
genRoot="./lib"
13+
14+
# Clean up any previously-generated bindings so stale types don't linger when
15+
# an assembly is removed or renamed upstream.
16+
rm -rf "${genRoot}"
17+
18+
# `--code-only` skips `gem build`, which would require Ruby + RubyGems to be
19+
# present in every environment that runs `yarn build`. The runtime tests do
20+
# not consume a built .gem — they load the generated sources under lib/
21+
# directly via $LOAD_PATH (see spec/spec_helper.rb).
22+
#
23+
# `--recurse` pacmaks jsii-calc *and* the whole dependency closure
24+
# (@scope/jsii-calc-{lib,base,base-of-base}), so we only need to pass the
25+
# top-level assembly here.
26+
yarn jsii-pacmak -t ruby --code-only --recurse -o "${genRoot}" ../../jsii-calc
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"name": "@jsii/ruby-runtime-test",
3+
"version": "0.0.0",
4+
"private": true,
5+
"description": "Tests for the Ruby client for jsii runtime",
6+
"license": "Apache-2.0",
7+
"author": {
8+
"name": "Amazon Web Services",
9+
"url": "https://aws.amazon.com"
10+
},
11+
"homepage": "https://github.com/aws/jsii",
12+
"bugs": {
13+
"url": "https://github.com/aws/jsii/issues"
14+
},
15+
"repository": {
16+
"type": "git",
17+
"url": "https://github.com/aws/jsii.git",
18+
"directory": "packages/@jsii/ruby-runtime-test"
19+
},
20+
"engines": {
21+
"node": ">= 14.17.0"
22+
},
23+
"scripts": {
24+
"build": "bash ./generate.sh",
25+
"test": "bash -c 'bash ./generate.sh && bundle install --quiet && bundle exec rspec --require ./spec/compliance_formatter.rb --format ComplianceFormatter --format progress'"
26+
},
27+
"devDependencies": {
28+
"@jsii/ruby-runtime": "^0.0.0",
29+
"@scope/jsii-calc-base": "^0.0.0",
30+
"@scope/jsii-calc-base-of-base": "^2.1.1",
31+
"@scope/jsii-calc-lib": "^0.0.0",
32+
"jsii-calc": "^3.20.120",
33+
"jsii-pacmak": "^0.0.0"
34+
}
35+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# frozen_string_literal: true
2+
3+
require 'spec_helper'
4+
5+
# Suite tests: abstractMembersAreCorrectlyHandled, returnAbstract,
6+
# unmarshallIntoAbstractType.
7+
#
8+
# Abstractness only exists at the TypeScript level — on the wire there are
9+
# just object refs and member names, and the Ruby proxy generated for an
10+
# abstract class has a concrete forwarding stub for *every* member (abstract
11+
# or not). These tests verify both directions: a guest subclass supplying
12+
# the abstract members to a host driver (abstractMembersAreCorrectlyHandled,
13+
# via callbacks), and the kernel returning values *declared* as abstract
14+
# types (returnAbstract, unmarshallIntoAbstractType).
15+
RSpec.describe 'JSII compliance: abstract types' do
16+
it 'handles abstract properties and methods correctly', compliance: 'abstractMembersAreCorrectlyHandled' do
17+
klass = Class.new(JsiiCalc::AbstractSuite) do
18+
def initialize
19+
super
20+
@property_val = nil
21+
end
22+
23+
def some_method(str)
24+
"Wrapped<#{str}>"
25+
end
26+
27+
def property
28+
@property_val
29+
end
30+
31+
def property=(value)
32+
@property_val = "String<#{value}>"
33+
end
34+
end
35+
36+
abstract_suite = klass.new
37+
expect(abstract_suite.work_it_all('Oomf!')).to eq('Wrapped<String<Oomf!>>')
38+
end
39+
40+
# The TS fixture (jsii-calc/lib/compliance.ts) behind this test:
41+
#
42+
# export abstract class AbstractClassBase { abstract readonly abstractProperty: string; }
43+
# export abstract class AbstractClass extends AbstractClassBase
44+
# implements IInterfaceImplementedByAbstractClass { ... }
45+
# class ConcreteClass extends AbstractClass { ... } // NOT exported!
46+
# export class AbstractClassReturner {
47+
# giveMeAbstract(): AbstractClass { return new ConcreteClass(); }
48+
# giveMeInterface(): IInterfaceImplementedByAbstractClass { return new ConcreteClass(); }
49+
# get returnAbstractFromProperty(): AbstractClassBase {
50+
# return { abstractProperty: 'hello-abstract-property' }; // plain object literal!
51+
# }
52+
# }
53+
#
54+
# This exercises three progressively nastier hydration cases — see the
55+
# inline comments. In all of them the key trick is that
56+
# Registry#build_uninitialized_instance hydrates refs with `klass.allocate`
57+
# (NOT `klass.new`), so no constructor runs and "you cannot instantiate an
58+
# abstract class" never comes up.
59+
it 'returns abstract classes and interfaces from the kernel', compliance: 'returnAbstract' do
60+
obj = JsiiCalc::AbstractClassReturner.new
61+
62+
# Case 1: the kernel returns an instance of *unexported* ConcreteClass,
63+
# declared as the *abstract* AbstractClass. ConcreteClass isn't in the
64+
# assembly, so the kernel labels the ref with the nearest exported
65+
# ancestor: `$jsii.byref: "jsii-calc.AbstractClass@..."`. The registry
66+
# resolves that fqn to the generated JsiiCalc::AbstractClass proxy and
67+
# allocates it without calling initialize.
68+
obj2 = obj.give_me_abstract
69+
70+
# Calls through the abstract proxy are plain virtual dispatch in JS: the
71+
# kernel invokes the member on the *real* object, so ConcreteClass's
72+
# implementation of the abstract method runs...
73+
expect(obj2.abstract_method('John')).to eq('Hello, John!!')
74+
# ...the interface-provided getter on AbstractClass answers a `get`...
75+
expect(obj2.prop_from_interface).to eq('propFromInterfaceValue')
76+
# ...and the inherited non-abstract base implementation works too.
77+
expect(obj2.non_abstract_method).to eq(42)
78+
79+
# Case 2: same ConcreteClass instance, but the declared return type is a
80+
# pure interface. Interface fqns register as Ruby *modules*, which can't
81+
# be allocated — the registry builds `Jsii::Object.allocate.extend(mod)`
82+
# instead, so the module's forwarding stubs (and is_a? checks) work.
83+
iface = obj.give_me_interface
84+
expect(iface.prop_from_interface).to eq('propFromInterfaceValue')
85+
86+
# Case 3: the JS getter returns a plain `{ abstractProperty: ... }`
87+
# object literal — no class at all. The kernel still wraps it in a ref,
88+
# but labels it "Object"; Registry#jsii_deserialize's fallback chain
89+
# (`$jsii.interfaces`.first, then the ref's fqn prefix) picks the declared
90+
# abstract type so the property read can be forwarded.
91+
expect(obj.return_abstract_from_property.abstract_property).to eq('hello-abstract-property')
92+
end
93+
94+
it 'unmarshalls values into abstract types', compliance: 'unmarshallIntoAbstractType' do
95+
calc = JsiiCalc::Calculator.new
96+
calc.add(120)
97+
expect(calc.curr.value).to eq(120)
98+
end
99+
100+
describe 'abstract types (extended)' do
101+
# The jsii-relevant part of subclassing an abstract host class is
102+
# construction: the kernel must instantiate an *abstract* fqn on behalf
103+
# of the guest by synthesizing a JS subclass, with the guest's overrides
104+
# registered (see object/overrides.rb). Calling the overridden members
105+
# from Ruby would only exercise plain Ruby dispatch — host-driven
106+
# callback dispatch is covered by abstractMembersAreCorrectlyHandled
107+
# above — so instead we call the members the guest did NOT override:
108+
# those go through the generated forwarding stubs to the kernel, proving
109+
# the instance is live on the JS side.
110+
it 'can construct native subclasses of abstract host classes' do
111+
klass = Class.new(JsiiCalc::AbstractClass) do
112+
def abstract_method(name)
113+
"Hello, #{name}!"
114+
end
115+
116+
def abstract_property
117+
'native-abstract-property'
118+
end
119+
end
120+
121+
obj = klass.new
122+
expect(obj.jsii_ref).not_to be_nil
123+
124+
# Non-overridden members dispatch through the kernel to the host
125+
# implementations on AbstractClass.
126+
expect(obj.non_abstract_method).to eq(42)
127+
expect(obj.prop_from_interface).to eq('propFromInterfaceValue')
128+
end
129+
end
130+
end

0 commit comments

Comments
 (0)