mockserver-client 7.1.0 → 7.3.0

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.
@@ -3,6 +3,7 @@
3
3
  require 'base64'
4
4
  require 'json'
5
5
  require 'set'
6
+ require 'erb'
6
7
 
7
8
  module MockServer
8
9
  # Explicit mapping from Ruby snake_case field names to the camelCase
@@ -30,6 +31,7 @@ module MockServer
30
31
  'response_callback' => 'responseCallback',
31
32
  'drop_connection' => 'dropConnection',
32
33
  'response_bytes' => 'responseBytes',
34
+ 'stream_error' => 'streamError',
33
35
  'http_request' => 'httpRequest',
34
36
  'http_response' => 'httpResponse',
35
37
  'http_response_template' => 'httpResponseTemplate',
@@ -84,7 +86,16 @@ module MockServer
84
86
  'http_class_callback' => 'httpClassCallback',
85
87
  'http_object_callback' => 'httpObjectCallback',
86
88
  'failure_policy' => 'failurePolicy',
87
- 'http_responses' => 'httpResponses'
89
+ 'http_responses' => 'httpResponses',
90
+ 'response_mode' => 'responseMode',
91
+ 'response_weights' => 'responseWeights',
92
+ 'switch_after' => 'switchAfter',
93
+ 'cross_protocol_scenarios' => 'crossProtocolScenarios',
94
+ 'scenario_name' => 'scenarioName',
95
+ 'scenario_state' => 'scenarioState',
96
+ 'new_scenario_state' => 'newScenarioState',
97
+ 'match_pattern' => 'matchPattern',
98
+ 'target_state' => 'targetState'
88
99
  }.freeze
89
100
 
90
101
  REVERSE_FIELD_MAP = FIELD_MAP.invert.freeze
@@ -119,6 +130,30 @@ module MockServer
119
130
  hash.reject { |_k, v| v.nil? }
120
131
  end
121
132
 
133
+ # @api private
134
+ # Coerce a class-callback value into an {HttpClassCallback}. Accepts:
135
+ # * +nil+ -> +nil+
136
+ # * a +String+ -> +HttpClassCallback.new(callback_class: <string>)+
137
+ # * an HttpClassCallback -> returned unchanged
138
+ # Any other type raises a TypeError. This lets the Expectation setters and the
139
+ # fluent builder accept either a fully-qualified class-name String or a
140
+ # pre-built {HttpClassCallback} (carrying +delay+ / +primary+).
141
+ def self.coerce_class_callback(value)
142
+ return nil if value.nil?
143
+ return HttpClassCallback.new(callback_class: value) if value.is_a?(String)
144
+ return value if value.is_a?(HttpClassCallback)
145
+
146
+ raise TypeError,
147
+ "Expected a class-name String or HttpClassCallback, got #{value.class.name}"
148
+ end
149
+
150
+ # @api private
151
+ # Percent-encode a single URL path segment (e.g. a scenario name), encoding
152
+ # spaces as %20 (not +) so the segment is safe inside +/mockserver/scenario/{name}+.
153
+ def self.encode_path_segment(value)
154
+ ERB::Util.url_encode(value.to_s)
155
+ end
156
+
122
157
  # @api private
123
158
  def self.serialize_value(value)
124
159
  case value
@@ -188,6 +223,29 @@ module MockServer
188
223
  end
189
224
  end
190
225
 
226
+ # @api private
227
+ # Unlike headers / query parameters, MockServer represents cookies as a
228
+ # single-value {name => value} object map, not a [{name, values}] array.
229
+ def self.serialize_cookies(items)
230
+ return nil if items.nil?
231
+
232
+ items.each_with_object({}) do |item, map|
233
+ map[item.name] = item.values.is_a?(Array) ? item.values.first : item.values
234
+ end
235
+ end
236
+
237
+ # @api private
238
+ def self.deserialize_cookies(data)
239
+ return nil if data.nil?
240
+
241
+ if data.is_a?(Hash)
242
+ return data.map { |k, v| KeyToMultiValue.new(name: k, values: v.is_a?(Array) ? v : [v]) }
243
+ end
244
+
245
+ # tolerate the legacy array form on read
246
+ deserialize_key_multi_values(data)
247
+ end
248
+
191
249
  # -------------------------------------------------------------------
192
250
  # Model classes
193
251
  # -------------------------------------------------------------------
@@ -564,7 +622,7 @@ module MockServer
564
622
  'path' => @path,
565
623
  'queryStringParameters' => MockServer.serialize_key_multi_values(@query_string_parameters),
566
624
  'headers' => MockServer.serialize_key_multi_values(@headers),
567
- 'cookies' => MockServer.serialize_key_multi_values(@cookies),
625
+ 'cookies' => MockServer.serialize_cookies(@cookies),
568
626
  'body' => MockServer.serialize_body(@body),
569
627
  'secure' => @secure,
570
628
  'keepAlive' => @keep_alive,
@@ -582,7 +640,7 @@ module MockServer
582
640
  path: data['path'],
583
641
  query_string_parameters: MockServer.deserialize_key_multi_values(data['queryStringParameters']),
584
642
  headers: MockServer.deserialize_key_multi_values(data['headers']),
585
- cookies: MockServer.deserialize_key_multi_values(data['cookies']),
643
+ cookies: MockServer.deserialize_cookies(data['cookies']),
586
644
  body: MockServer.deserialize_body(data['body']),
587
645
  secure: data['secure'],
588
646
  keep_alive: data['keepAlive'],
@@ -714,7 +772,7 @@ module MockServer
714
772
  'statusCode' => @status_code,
715
773
  'reasonPhrase' => @reason_phrase,
716
774
  'headers' => MockServer.serialize_key_multi_values(@headers),
717
- 'cookies' => MockServer.serialize_key_multi_values(@cookies),
775
+ 'cookies' => MockServer.serialize_cookies(@cookies),
718
776
  'body' => MockServer.serialize_body(@body),
719
777
  'delay' => @delay&.to_h,
720
778
  'connectionOptions' => @connection_options&.to_h,
@@ -729,7 +787,7 @@ module MockServer
729
787
  status_code: data['statusCode'],
730
788
  reason_phrase: data['reasonPhrase'],
731
789
  headers: MockServer.deserialize_key_multi_values(data['headers']),
732
- cookies: MockServer.deserialize_key_multi_values(data['cookies']),
790
+ cookies: MockServer.deserialize_cookies(data['cookies']),
733
791
  body: MockServer.deserialize_body(data['body']),
734
792
  delay: Delay.from_hash(data['delay']),
735
793
  connection_options: ConnectionOptions.from_hash(data['connectionOptions']),
@@ -935,11 +993,15 @@ module MockServer
935
993
  end
936
994
 
937
995
  class HttpError
938
- attr_accessor :drop_connection, :response_bytes, :delay, :primary
996
+ # stream_error: reset the matched request stream with this error code (HTTP/2 RST_STREAM /
997
+ # HTTP/3 RESET_STREAM) instead of returning a response; HTTP/1.1 has no stream concept so this
998
+ # falls back to dropping the connection. Takes precedence over drop_connection when both are set.
999
+ attr_accessor :drop_connection, :response_bytes, :stream_error, :delay, :primary
939
1000
 
940
- def initialize(drop_connection: nil, response_bytes: nil, delay: nil, primary: nil)
1001
+ def initialize(drop_connection: nil, response_bytes: nil, stream_error: nil, delay: nil, primary: nil)
941
1002
  @drop_connection = drop_connection
942
1003
  @response_bytes = response_bytes
1004
+ @stream_error = stream_error
943
1005
  @delay = delay
944
1006
  @primary = primary
945
1007
  end
@@ -948,6 +1010,7 @@ module MockServer
948
1010
  MockServer.strip_none({
949
1011
  'dropConnection' => @drop_connection,
950
1012
  'responseBytes' => @response_bytes,
1013
+ 'streamError' => @stream_error,
951
1014
  'delay' => @delay&.to_h,
952
1015
  'primary' => @primary
953
1016
  })
@@ -959,6 +1022,7 @@ module MockServer
959
1022
  new(
960
1023
  drop_connection: data['dropConnection'],
961
1024
  response_bytes: data['responseBytes'],
1025
+ stream_error: data['streamError'],
962
1026
  delay: Delay.from_hash(data['delay']),
963
1027
  primary: data['primary']
964
1028
  )
@@ -1680,6 +1744,146 @@ module MockServer
1680
1744
  end
1681
1745
  end
1682
1746
 
1747
+ # The strategy used to pick which of an expectation's +http_responses+ is
1748
+ # returned for each matching request. Mirrors the core +ResponseMode+ enum.
1749
+ module ResponseMode
1750
+ SEQUENTIAL = 'SEQUENTIAL'
1751
+ RANDOM = 'RANDOM'
1752
+ WEIGHTED = 'WEIGHTED'
1753
+ SWITCH = 'SWITCH'
1754
+ end
1755
+
1756
+ # The protocol event that advances a cross-protocol scenario. Mirrors the
1757
+ # core +CrossProtocolTrigger+ enum.
1758
+ module CrossProtocolTrigger
1759
+ DNS_QUERY = 'DNS_QUERY'
1760
+ WEBSOCKET_CONNECT = 'WEBSOCKET_CONNECT'
1761
+ GRPC_REQUEST = 'GRPC_REQUEST'
1762
+ HTTP_REQUEST = 'HTTP_REQUEST'
1763
+ end
1764
+
1765
+ # Describes a cross-protocol scenario correlation: when a protocol event
1766
+ # matching +trigger+ (and optionally +match_pattern+) is observed, the named
1767
+ # scenario +scenario_name+ is advanced to +target_state+.
1768
+ class CrossProtocolScenario
1769
+ attr_accessor :trigger, :match_pattern, :scenario_name, :target_state
1770
+
1771
+ def initialize(trigger: nil, match_pattern: nil, scenario_name: nil, target_state: nil)
1772
+ @trigger = trigger
1773
+ @match_pattern = match_pattern
1774
+ @scenario_name = scenario_name
1775
+ @target_state = target_state
1776
+ end
1777
+
1778
+ def to_h
1779
+ MockServer.strip_none({
1780
+ 'trigger' => @trigger,
1781
+ 'matchPattern' => @match_pattern,
1782
+ 'scenarioName' => @scenario_name,
1783
+ 'targetState' => @target_state
1784
+ })
1785
+ end
1786
+
1787
+ def self.from_hash(data)
1788
+ return nil if data.nil?
1789
+
1790
+ new(
1791
+ trigger: data['trigger'],
1792
+ match_pattern: data['matchPattern'],
1793
+ scenario_name: data['scenarioName'],
1794
+ target_state: data['targetState']
1795
+ )
1796
+ end
1797
+ end
1798
+
1799
+ # The state of a single named scenario as reported by the server's scenario
1800
+ # control plane. +scenario_name+ is the state-machine name and +current_state+
1801
+ # is its current state (+nil+ if not yet set).
1802
+ class ScenarioState
1803
+ attr_accessor :scenario_name, :current_state, :next_state, :transition_after_ms
1804
+
1805
+ def initialize(scenario_name: nil, current_state: nil, next_state: nil, transition_after_ms: nil)
1806
+ @scenario_name = scenario_name
1807
+ @current_state = current_state
1808
+ @next_state = next_state
1809
+ @transition_after_ms = transition_after_ms
1810
+ end
1811
+
1812
+ def to_h
1813
+ MockServer.strip_none({
1814
+ 'scenarioName' => @scenario_name,
1815
+ 'currentState' => @current_state,
1816
+ 'nextState' => @next_state,
1817
+ 'transitionAfterMs' => @transition_after_ms
1818
+ })
1819
+ end
1820
+
1821
+ def self.from_hash(data)
1822
+ return nil if data.nil?
1823
+
1824
+ new(
1825
+ scenario_name: data['scenarioName'],
1826
+ current_state: data['currentState'],
1827
+ next_state: data['nextState'],
1828
+ transition_after_ms: data['transitionAfterMs']
1829
+ )
1830
+ end
1831
+ end
1832
+
1833
+ # A handle to a single named stateful scenario on the server, wrapping the
1834
+ # +/mockserver/scenario/{name}+ control-plane endpoints.
1835
+ #
1836
+ # Obtained via {Client#scenario}:
1837
+ # client.scenario('Deploy').set('Deploying', transition_after_ms: 5000, next_state: 'Deployed')
1838
+ # client.scenario('Deploy').trigger('Failed')
1839
+ # client.scenario('Deploy').state # => "Failed"
1840
+ class ScenarioHandle
1841
+ attr_reader :name
1842
+
1843
+ def initialize(client, name)
1844
+ @client = client
1845
+ @name = name
1846
+ end
1847
+
1848
+ # GET the current state of this scenario (+nil+ if not yet set).
1849
+ # @return [String, nil]
1850
+ def state
1851
+ result = @client.scenario_request('GET', scenario_path)
1852
+ result['currentState']
1853
+ end
1854
+
1855
+ # PUT to set this scenario's state, optionally scheduling a timed transition
1856
+ # to +next_state+ after +transition_after_ms+ milliseconds.
1857
+ #
1858
+ # @param state [String] the state to set
1859
+ # @param transition_after_ms [Integer, nil] delay before auto-transitioning
1860
+ # @param next_state [String, nil] the state to auto-transition to
1861
+ # @return [ScenarioState]
1862
+ def set(state, transition_after_ms: nil, next_state: nil)
1863
+ payload = { 'state' => state }
1864
+ payload['transitionAfterMs'] = transition_after_ms unless transition_after_ms.nil?
1865
+ payload['nextState'] = next_state unless next_state.nil?
1866
+ result = @client.scenario_request('PUT', scenario_path, JSON.generate(payload))
1867
+ ScenarioState.from_hash(result)
1868
+ end
1869
+
1870
+ # PUT an external trigger advancing this scenario to +new_state+.
1871
+ #
1872
+ # @param new_state [String]
1873
+ # @return [ScenarioState]
1874
+ def trigger(new_state)
1875
+ body = JSON.generate({ 'newState' => new_state })
1876
+ result = @client.scenario_request('PUT', "#{scenario_path}/trigger", body)
1877
+ ScenarioState.from_hash(result)
1878
+ end
1879
+
1880
+ private
1881
+
1882
+ def scenario_path
1883
+ "/mockserver/scenario/#{MockServer.encode_path_segment(@name)}"
1884
+ end
1885
+ end
1886
+
1683
1887
  class Expectation
1684
1888
  attr_accessor :id, :priority, :percentage, :http_request, :http_response,
1685
1889
  :http_response_template, :http_response_class_callback,
@@ -1691,7 +1895,8 @@ module MockServer
1691
1895
  :grpc_stream_response, :grpc_bidi_response,
1692
1896
  :binary_response, :dns_response,
1693
1897
  :before_actions, :after_actions,
1694
- :http_responses, :response_mode, :steps,
1898
+ :http_responses, :response_mode, :response_weights, :switch_after,
1899
+ :cross_protocol_scenarios, :steps,
1695
1900
  :scenario_name, :scenario_state, :new_scenario_state
1696
1901
 
1697
1902
  def initialize(id: nil, priority: nil, percentage: nil, http_request: nil, http_response: nil,
@@ -1704,7 +1909,8 @@ module MockServer
1704
1909
  grpc_stream_response: nil, grpc_bidi_response: nil,
1705
1910
  binary_response: nil, dns_response: nil,
1706
1911
  before_actions: nil, after_actions: nil,
1707
- http_responses: nil, response_mode: nil, steps: nil,
1912
+ http_responses: nil, response_mode: nil, response_weights: nil,
1913
+ switch_after: nil, cross_protocol_scenarios: nil, steps: nil,
1708
1914
  scenario_name: nil, scenario_state: nil, new_scenario_state: nil)
1709
1915
  @id = id
1710
1916
  @priority = priority
@@ -1712,11 +1918,14 @@ module MockServer
1712
1918
  @http_request = http_request
1713
1919
  @http_response = http_response
1714
1920
  @http_response_template = http_response_template
1715
- @http_response_class_callback = http_response_class_callback
1921
+ # Route the two class-callback fields through their setters so a bare
1922
+ # class-name String passed to the constructor is coerced to an
1923
+ # HttpClassCallback (matching the setter / fluent-builder behaviour).
1924
+ self.http_response_class_callback = http_response_class_callback
1716
1925
  @http_response_object_callback = http_response_object_callback
1717
1926
  @http_forward = http_forward
1718
1927
  @http_forward_template = http_forward_template
1719
- @http_forward_class_callback = http_forward_class_callback
1928
+ self.http_forward_class_callback = http_forward_class_callback
1720
1929
  @http_forward_object_callback = http_forward_object_callback
1721
1930
  @http_override_forwarded_request = http_override_forwarded_request
1722
1931
  @http_error = http_error
@@ -1733,12 +1942,31 @@ module MockServer
1733
1942
  @after_actions = after_actions
1734
1943
  @http_responses = http_responses
1735
1944
  @response_mode = response_mode
1945
+ @response_weights = response_weights
1946
+ @switch_after = switch_after
1947
+ @cross_protocol_scenarios = cross_protocol_scenarios
1736
1948
  @steps = steps
1737
1949
  @scenario_name = scenario_name
1738
1950
  @scenario_state = scenario_state
1739
1951
  @new_scenario_state = new_scenario_state
1740
1952
  end
1741
1953
 
1954
+ # Set the response class-callback action. Accepts either a fully-qualified
1955
+ # class-name String (e.g. "com.example.MyResponseCallback") or a pre-built
1956
+ # {HttpClassCallback} (carrying an optional +delay+ / +primary+). A String is
1957
+ # wrapped into an {HttpClassCallback} so +to_h+ always emits
1958
+ # +httpResponseClassCallback.callbackClass+.
1959
+ def http_response_class_callback=(value)
1960
+ @http_response_class_callback = MockServer.coerce_class_callback(value)
1961
+ end
1962
+
1963
+ # Set the forward class-callback action. Accepts either a fully-qualified
1964
+ # class-name String or a pre-built {HttpClassCallback}; serialized as
1965
+ # +httpForwardClassCallback+.
1966
+ def http_forward_class_callback=(value)
1967
+ @http_forward_class_callback = MockServer.coerce_class_callback(value)
1968
+ end
1969
+
1742
1970
  def to_h
1743
1971
  before_actions_h = nil
1744
1972
  if @before_actions.is_a?(Array)
@@ -1779,6 +2007,9 @@ module MockServer
1779
2007
  'afterActions' => after_actions_h,
1780
2008
  'httpResponses' => @http_responses&.map(&:to_h),
1781
2009
  'responseMode' => @response_mode,
2010
+ 'responseWeights' => @response_weights,
2011
+ 'switchAfter' => @switch_after,
2012
+ 'crossProtocolScenarios' => @cross_protocol_scenarios&.map(&:to_h),
1782
2013
  'steps' => @steps&.map(&:to_h),
1783
2014
  'times' => @times&.to_h,
1784
2015
  'timeToLive' => @time_to_live&.to_h,
@@ -1831,6 +2062,9 @@ module MockServer
1831
2062
  after_actions: after_actions,
1832
2063
  http_responses: data['httpResponses']&.map { |r| HttpResponse.from_hash(r) },
1833
2064
  response_mode: data['responseMode'],
2065
+ response_weights: data['responseWeights'],
2066
+ switch_after: data['switchAfter'],
2067
+ cross_protocol_scenarios: data['crossProtocolScenarios']&.map { |c| CrossProtocolScenario.from_hash(c) },
1834
2068
  steps: data['steps']&.map { |s| ExpectationStep.from_hash(s) },
1835
2069
  times: Times.from_hash(data['times']),
1836
2070
  time_to_live: TimeToLive.from_hash(data['timeToLive']),
@@ -2053,6 +2287,442 @@ module MockServer
2053
2287
  end
2054
2288
  end
2055
2289
 
2290
+ # One stage of a {LoadProfile}, run in sequence. Each stage holds or ramps a
2291
+ # setpoint for +duration_millis+:
2292
+ #
2293
+ # * +VU+ (closed model) - hold +vus+ virtual users, or ramp +start_vus+ to
2294
+ # +end_vus+ along +curve+.
2295
+ # * +RATE+ (open model) - hold +rate+ iterations/second, or ramp +start_rate+
2296
+ # to +end_rate+ along +curve+, optionally capping the auto-scaling
2297
+ # virtual-user pool at +max_vus+.
2298
+ # * +PAUSE+ - drive no load for +duration_millis+.
2299
+ #
2300
+ # Prefer the {.vu}, {.rate} and {.pause} factories, which emit only the fields
2301
+ # relevant to the stage type and mode.
2302
+ class LoadStage
2303
+ attr_accessor :type, :duration_millis, :curve, :vus, :start_vus, :end_vus,
2304
+ :rate, :start_rate, :end_rate, :max_vus
2305
+
2306
+ def initialize(type:, duration_millis:, curve: nil, vus: nil, start_vus: nil,
2307
+ end_vus: nil, rate: nil, start_rate: nil, end_rate: nil, max_vus: nil)
2308
+ @type = type
2309
+ @duration_millis = duration_millis
2310
+ @curve = curve
2311
+ @vus = vus
2312
+ @start_vus = start_vus
2313
+ @end_vus = end_vus
2314
+ @rate = rate
2315
+ @start_rate = start_rate
2316
+ @end_rate = end_rate
2317
+ @max_vus = max_vus
2318
+ end
2319
+
2320
+ # A +VU+ (closed-model) stage - hold +vus+ or ramp +start_vus+ to +end_vus+.
2321
+ def self.vu(duration_millis, vus: nil, start_vus: nil, end_vus: nil, curve: nil)
2322
+ new(type: 'VU', duration_millis: duration_millis, vus: vus,
2323
+ start_vus: start_vus, end_vus: end_vus, curve: curve)
2324
+ end
2325
+
2326
+ # A +RATE+ (open-model) stage - hold +rate+ or ramp +start_rate+ to
2327
+ # +end_rate+ (iterations/second).
2328
+ def self.rate(duration_millis, rate: nil, start_rate: nil, end_rate: nil, max_vus: nil, curve: nil)
2329
+ new(type: 'RATE', duration_millis: duration_millis, rate: rate,
2330
+ start_rate: start_rate, end_rate: end_rate, max_vus: max_vus, curve: curve)
2331
+ end
2332
+
2333
+ # A +PAUSE+ stage - drive no load for +duration_millis+.
2334
+ def self.pause(duration_millis)
2335
+ new(type: 'PAUSE', duration_millis: duration_millis)
2336
+ end
2337
+
2338
+ def to_h
2339
+ MockServer.strip_none({
2340
+ 'type' => @type,
2341
+ 'durationMillis' => @duration_millis,
2342
+ 'curve' => @curve,
2343
+ 'vus' => @vus,
2344
+ 'startVus' => @start_vus,
2345
+ 'endVus' => @end_vus,
2346
+ 'rate' => @rate,
2347
+ 'startRate' => @start_rate,
2348
+ 'endRate' => @end_rate,
2349
+ 'maxVus' => @max_vus
2350
+ })
2351
+ end
2352
+
2353
+ def self.from_hash(data)
2354
+ return nil if data.nil?
2355
+
2356
+ new(
2357
+ type: data['type'],
2358
+ duration_millis: data['durationMillis'],
2359
+ curve: data['curve'],
2360
+ vus: data['vus'],
2361
+ start_vus: data['startVus'],
2362
+ end_vus: data['endVus'],
2363
+ rate: data['rate'],
2364
+ start_rate: data['startRate'],
2365
+ end_rate: data['endRate'],
2366
+ max_vus: data['maxVus']
2367
+ )
2368
+ end
2369
+ end
2370
+
2371
+ # A declarative named load shape that expands into ordinary {LoadStage} stages.
2372
+ # Set this as a {LoadProfile}'s +shape+ instead of an explicit +stages+ list;
2373
+ # only the parameters its +type+ needs are read. +type+ is one of +SPIKE+,
2374
+ # +STAIRS+ or +RAMP_HOLD+; +metric+ (+VU+ or +RATE+) selects what the shape
2375
+ # drives.
2376
+ class LoadShape
2377
+ attr_accessor :type, :metric, :curve, :baseline, :peak, :ramp_up_millis,
2378
+ :hold_millis, :ramp_down_millis, :recovery_hold_millis,
2379
+ :start, :step, :steps, :step_duration_millis, :target, :ramp_millis
2380
+
2381
+ def initialize(type:, metric: nil, curve: nil, baseline: nil, peak: nil,
2382
+ ramp_up_millis: nil, hold_millis: nil, ramp_down_millis: nil,
2383
+ recovery_hold_millis: nil, start: nil, step: nil, steps: nil,
2384
+ step_duration_millis: nil, target: nil, ramp_millis: nil)
2385
+ @type = type
2386
+ @metric = metric
2387
+ @curve = curve
2388
+ @baseline = baseline
2389
+ @peak = peak
2390
+ @ramp_up_millis = ramp_up_millis
2391
+ @hold_millis = hold_millis
2392
+ @ramp_down_millis = ramp_down_millis
2393
+ @recovery_hold_millis = recovery_hold_millis
2394
+ @start = start
2395
+ @step = step
2396
+ @steps = steps
2397
+ @step_duration_millis = step_duration_millis
2398
+ @target = target
2399
+ @ramp_millis = ramp_millis
2400
+ end
2401
+
2402
+ def to_h
2403
+ MockServer.strip_none({
2404
+ 'type' => @type,
2405
+ 'metric' => @metric,
2406
+ 'curve' => @curve,
2407
+ 'baseline' => @baseline,
2408
+ 'peak' => @peak,
2409
+ 'rampUpMillis' => @ramp_up_millis,
2410
+ 'holdMillis' => @hold_millis,
2411
+ 'rampDownMillis' => @ramp_down_millis,
2412
+ 'recoveryHoldMillis' => @recovery_hold_millis,
2413
+ 'start' => @start,
2414
+ 'step' => @step,
2415
+ 'steps' => @steps,
2416
+ 'stepDurationMillis' => @step_duration_millis,
2417
+ 'target' => @target,
2418
+ 'rampMillis' => @ramp_millis
2419
+ })
2420
+ end
2421
+
2422
+ def self.from_hash(data)
2423
+ return nil if data.nil?
2424
+
2425
+ new(
2426
+ type: data['type'],
2427
+ metric: data['metric'],
2428
+ curve: data['curve'],
2429
+ baseline: data['baseline'],
2430
+ peak: data['peak'],
2431
+ ramp_up_millis: data['rampUpMillis'],
2432
+ hold_millis: data['holdMillis'],
2433
+ ramp_down_millis: data['rampDownMillis'],
2434
+ recovery_hold_millis: data['recoveryHoldMillis'],
2435
+ start: data['start'],
2436
+ step: data['step'],
2437
+ steps: data['steps'],
2438
+ step_duration_millis: data['stepDurationMillis'],
2439
+ target: data['target'],
2440
+ ramp_millis: data['rampMillis']
2441
+ )
2442
+ end
2443
+ end
2444
+
2445
+ # The traffic-shaping profile of a load scenario: EITHER an ordered list of
2446
+ # {LoadStage} objects run in sequence, each holding or ramping a setpoint
2447
+ # (virtual users, an arrival rate, or a pause) for its duration, OR a single
2448
+ # named {LoadShape} that expands into stages. Set one, not both; if both are
2449
+ # set the explicit stages win. The total run length is the sum of the stage
2450
+ # durations.
2451
+ class LoadProfile
2452
+ attr_accessor :stages, :shape
2453
+
2454
+ def initialize(stages: nil, shape: nil)
2455
+ @stages = stages
2456
+ @shape = shape
2457
+ end
2458
+
2459
+ def to_h
2460
+ MockServer.strip_none({
2461
+ 'stages' => @stages.nil? ? nil : @stages.map(&:to_h),
2462
+ 'shape' => @shape&.to_h
2463
+ })
2464
+ end
2465
+
2466
+ def self.from_hash(data)
2467
+ return nil if data.nil?
2468
+
2469
+ stages_data = data['stages']
2470
+ new(
2471
+ stages: stages_data ? stages_data.map { |s| LoadStage.from_hash(s) } : nil,
2472
+ shape: LoadShape.from_hash(data['shape'])
2473
+ )
2474
+ end
2475
+ end
2476
+
2477
+ # A declarative cross-step capture / correlation rule: extracts a value from a
2478
+ # step's response and binds it to +name+, which a later step in the same
2479
+ # iteration can reference from its templated request fields. +source+ is one of
2480
+ # +BODY_JSONPATH+, +HEADER+ or +BODY_REGEX+; +expression+ drives the extraction;
2481
+ # +default_value+ is an optional fallback when extraction yields nothing.
2482
+ class LoadCapture
2483
+ attr_accessor :name, :source, :expression, :default_value
2484
+
2485
+ def initialize(name:, source:, expression:, default_value: nil)
2486
+ @name = name
2487
+ @source = source
2488
+ @expression = expression
2489
+ @default_value = default_value
2490
+ end
2491
+
2492
+ def to_h
2493
+ MockServer.strip_none({
2494
+ 'name' => @name,
2495
+ 'source' => @source,
2496
+ 'expression' => @expression,
2497
+ 'defaultValue' => @default_value
2498
+ })
2499
+ end
2500
+
2501
+ def self.from_hash(data)
2502
+ return nil if data.nil?
2503
+
2504
+ new(
2505
+ name: data['name'],
2506
+ source: data['source'],
2507
+ expression: data['expression'],
2508
+ default_value: data['defaultValue']
2509
+ )
2510
+ end
2511
+ end
2512
+
2513
+ # An in-run pass/fail threshold for a load scenario: a per-run +metric+ compared
2514
+ # with +comparator+ against +threshold+. All thresholds must hold for the run
2515
+ # verdict to be +PASS+; any breach makes it +FAIL+. +metric+ is one of
2516
+ # +LATENCY_P50+/+LATENCY_P95+/+LATENCY_P99+/+LATENCY_P999+/+ERROR_RATE+/
2517
+ # +THROUGHPUT_RPS+; +comparator+ is one of +LESS_THAN+/+LESS_THAN_OR_EQUAL+/
2518
+ # +GREATER_THAN+/+GREATER_THAN_OR_EQUAL+.
2519
+ class LoadThreshold
2520
+ attr_accessor :metric, :comparator, :threshold
2521
+
2522
+ def initialize(metric:, comparator:, threshold:)
2523
+ @metric = metric
2524
+ @comparator = comparator
2525
+ @threshold = threshold
2526
+ end
2527
+
2528
+ def to_h
2529
+ MockServer.strip_none({
2530
+ 'metric' => @metric,
2531
+ 'comparator' => @comparator,
2532
+ 'threshold' => @threshold
2533
+ })
2534
+ end
2535
+
2536
+ def self.from_hash(data)
2537
+ return nil if data.nil?
2538
+
2539
+ new(
2540
+ metric: data['metric'],
2541
+ comparator: data['comparator'],
2542
+ threshold: data['threshold']
2543
+ )
2544
+ end
2545
+ end
2546
+
2547
+ # Adaptive iteration pacing (think-time) for a load scenario: a target
2548
+ # per-virtual-user iteration cycle time. +mode+ is one of +NONE+,
2549
+ # +CONSTANT_PACING+ (+value+ is the target cycle in milliseconds) or
2550
+ # +CONSTANT_THROUGHPUT+ (+value+ is the target iterations/second per VU).
2551
+ class LoadPacing
2552
+ attr_accessor :mode, :value
2553
+
2554
+ def initialize(mode:, value: nil)
2555
+ @mode = mode
2556
+ @value = value
2557
+ end
2558
+
2559
+ def to_h
2560
+ MockServer.strip_none({
2561
+ 'mode' => @mode,
2562
+ 'value' => @value
2563
+ })
2564
+ end
2565
+
2566
+ def self.from_hash(data)
2567
+ return nil if data.nil?
2568
+
2569
+ new(
2570
+ mode: data['mode'],
2571
+ value: data['value']
2572
+ )
2573
+ end
2574
+ end
2575
+
2576
+ # Parameterized test data (a data feeder) for a load scenario: an inline dataset
2577
+ # from which one row is selected per iteration and exposed to the templated
2578
+ # request. Supply EITHER +rows+ (inline list of column-name to value maps, the
2579
+ # primary form) OR +data+ + +format+ (raw CSV/JSON parsed server-side).
2580
+ # +strategy+ (+CIRCULAR+, +RANDOM+ or +SEQUENTIAL+) chooses how a row is picked.
2581
+ class LoadFeeder
2582
+ attr_accessor :rows, :data, :format, :strategy
2583
+
2584
+ def initialize(rows: nil, data: nil, format: nil, strategy: nil)
2585
+ @rows = rows
2586
+ @data = data
2587
+ @format = format
2588
+ @strategy = strategy
2589
+ end
2590
+
2591
+ def to_h
2592
+ MockServer.strip_none({
2593
+ 'rows' => @rows,
2594
+ 'data' => @data,
2595
+ 'format' => @format,
2596
+ 'strategy' => @strategy
2597
+ })
2598
+ end
2599
+
2600
+ def self.from_hash(data)
2601
+ return nil if data.nil?
2602
+
2603
+ new(
2604
+ rows: data['rows'],
2605
+ data: data['data'],
2606
+ format: data['format'],
2607
+ strategy: data['strategy']
2608
+ )
2609
+ end
2610
+ end
2611
+
2612
+ # A single step within a load scenario. Each step fires +request+ (an HttpRequest)
2613
+ # against the target, optionally pausing for +think_time+ (a Delay) afterwards.
2614
+ # +captures+ binds values from this step's response for later steps in the same
2615
+ # iteration; +weight+ is the relative selection weight when the scenario's
2616
+ # +step_selection+ is +WEIGHTED+.
2617
+ class LoadStep
2618
+ attr_accessor :name, :labels, :think_time, :request, :captures, :weight
2619
+
2620
+ def initialize(request:, name: nil, labels: nil, think_time: nil, captures: nil, weight: nil)
2621
+ @request = request
2622
+ @name = name
2623
+ @labels = labels
2624
+ @think_time = think_time
2625
+ @captures = captures
2626
+ @weight = weight
2627
+ end
2628
+
2629
+ def to_h
2630
+ MockServer.strip_none({
2631
+ 'name' => @name,
2632
+ 'labels' => @labels,
2633
+ 'thinkTime' => @think_time&.to_h,
2634
+ 'request' => @request&.to_h,
2635
+ 'captures' => @captures.nil? ? nil : @captures.map(&:to_h),
2636
+ 'weight' => @weight
2637
+ })
2638
+ end
2639
+
2640
+ def self.from_hash(data)
2641
+ return nil if data.nil?
2642
+
2643
+ captures_data = data['captures']
2644
+ new(
2645
+ name: data['name'],
2646
+ labels: data['labels'],
2647
+ think_time: Delay.from_hash(data['thinkTime']),
2648
+ request: HttpRequest.from_hash(data['request']),
2649
+ captures: captures_data ? captures_data.map { |c| LoadCapture.from_hash(c) } : nil,
2650
+ weight: data['weight']
2651
+ )
2652
+ end
2653
+ end
2654
+
2655
+ # A load-injection scenario: a named set of +steps+ driven by a traffic +profile+.
2656
+ # +template_type+ selects the templating engine (+VELOCITY+ or +MUSTACHE+) used to
2657
+ # render step requests; +max_requests+ caps the total requests issued.
2658
+ # +thresholds+ are in-run pass/fail checks; +abort_on_fail+/+abort_grace_millis+
2659
+ # control early-abort behaviour; +pacing+ shapes inter-iteration timing; +feeder+
2660
+ # supplies per-iteration data; +step_selection+ (+SEQUENTIAL+ or +WEIGHTED+)
2661
+ # controls how each iteration selects steps.
2662
+ class LoadScenario
2663
+ attr_accessor :name, :template_type, :labels, :max_requests, :start_delay_millis,
2664
+ :profile, :steps, :thresholds, :abort_on_fail, :abort_grace_millis,
2665
+ :pacing, :feeder, :step_selection
2666
+
2667
+ def initialize(name:, profile:, steps:, template_type: nil, labels: nil, max_requests: nil,
2668
+ start_delay_millis: nil, thresholds: nil, abort_on_fail: nil,
2669
+ abort_grace_millis: nil, pacing: nil, feeder: nil, step_selection: nil)
2670
+ @name = name
2671
+ @profile = profile
2672
+ @steps = steps
2673
+ @template_type = template_type
2674
+ @labels = labels
2675
+ @max_requests = max_requests
2676
+ @start_delay_millis = start_delay_millis
2677
+ @thresholds = thresholds
2678
+ @abort_on_fail = abort_on_fail
2679
+ @abort_grace_millis = abort_grace_millis
2680
+ @pacing = pacing
2681
+ @feeder = feeder
2682
+ @step_selection = step_selection
2683
+ end
2684
+
2685
+ def to_h
2686
+ MockServer.strip_none({
2687
+ 'name' => @name,
2688
+ 'templateType' => @template_type,
2689
+ 'labels' => @labels,
2690
+ 'maxRequests' => @max_requests,
2691
+ 'startDelayMillis' => @start_delay_millis,
2692
+ 'thresholds' => @thresholds.nil? ? nil : @thresholds.map(&:to_h),
2693
+ 'abortOnFail' => @abort_on_fail,
2694
+ 'abortGraceMillis' => @abort_grace_millis,
2695
+ 'pacing' => @pacing&.to_h,
2696
+ 'feeder' => @feeder&.to_h,
2697
+ 'stepSelection' => @step_selection,
2698
+ 'profile' => @profile&.to_h,
2699
+ 'steps' => @steps&.map(&:to_h)
2700
+ })
2701
+ end
2702
+
2703
+ def self.from_hash(data)
2704
+ return nil if data.nil?
2705
+
2706
+ steps_data = data['steps']
2707
+ thresholds_data = data['thresholds']
2708
+ new(
2709
+ name: data['name'],
2710
+ template_type: data['templateType'],
2711
+ labels: data['labels'],
2712
+ max_requests: data['maxRequests'],
2713
+ start_delay_millis: data['startDelayMillis'],
2714
+ thresholds: thresholds_data ? thresholds_data.map { |t| LoadThreshold.from_hash(t) } : nil,
2715
+ abort_on_fail: data['abortOnFail'],
2716
+ abort_grace_millis: data['abortGraceMillis'],
2717
+ pacing: LoadPacing.from_hash(data['pacing']),
2718
+ feeder: LoadFeeder.from_hash(data['feeder']),
2719
+ step_selection: data['stepSelection'],
2720
+ profile: LoadProfile.from_hash(data['profile']),
2721
+ steps: steps_data ? steps_data.map { |s| LoadStep.from_hash(s) } : nil
2722
+ )
2723
+ end
2724
+ end
2725
+
2056
2726
  # Alias matching the Python client
2057
2727
  RequestDefinition = HttpRequest
2058
2728
  end