pact_broker-client 1.41.0 → 1.46.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +21 -0
  3. data/CHANGELOG.md +57 -0
  4. data/README.md +69 -48
  5. data/doc/pacts/markdown/Pact Broker Client - Pact Broker.md +79 -102
  6. data/doc/pacts/markdown/Pact Broker Client - Pactflow.md +94 -0
  7. data/doc/pacts/markdown/README.md +1 -0
  8. data/lib/pact_broker/client/backports.rb +9 -0
  9. data/lib/pact_broker/client/base_command.rb +98 -0
  10. data/lib/pact_broker/client/can_i_deploy.rb +40 -5
  11. data/lib/pact_broker/client/cli/broker.rb +18 -160
  12. data/lib/pact_broker/client/cli/custom_thor.rb +12 -16
  13. data/lib/pact_broker/client/cli/deployment_commands.rb +94 -0
  14. data/lib/pact_broker/client/cli/environment_commands.rb +70 -0
  15. data/lib/pact_broker/client/cli/pacticipant_commands.rb +53 -0
  16. data/lib/pact_broker/client/cli/webhook_commands.rb +122 -0
  17. data/lib/pact_broker/client/deployments.rb +4 -0
  18. data/lib/pact_broker/client/deployments/record_deployment.rb +38 -0
  19. data/lib/pact_broker/client/deployments/record_release.rb +99 -0
  20. data/lib/pact_broker/client/deployments/record_support_ended.rb +103 -0
  21. data/lib/pact_broker/client/deployments/record_undeployment.rb +127 -0
  22. data/lib/pact_broker/client/describe_text_formatter.rb +23 -0
  23. data/lib/pact_broker/client/environments.rb +6 -0
  24. data/lib/pact_broker/client/environments/create_environment.rb +31 -0
  25. data/lib/pact_broker/client/environments/delete_environment.rb +27 -0
  26. data/lib/pact_broker/client/environments/describe_environment.rb +26 -0
  27. data/lib/pact_broker/client/environments/environment_command.rb +66 -0
  28. data/lib/pact_broker/client/environments/list_environments.rb +30 -0
  29. data/lib/pact_broker/client/environments/text_formatter.rb +30 -0
  30. data/lib/pact_broker/client/environments/update_environment.rb +31 -0
  31. data/lib/pact_broker/client/generate_display_name.rb +27 -0
  32. data/lib/pact_broker/client/hal/entity.rb +31 -6
  33. data/lib/pact_broker/client/hal/http_client.rb +8 -2
  34. data/lib/pact_broker/client/hal/link.rb +8 -0
  35. data/lib/pact_broker/client/hal_client_methods.rb +1 -3
  36. data/lib/pact_broker/client/matrix/text_formatter.rb +21 -13
  37. data/lib/pact_broker/client/pacticipants.rb +6 -0
  38. data/lib/pact_broker/client/pacticipants/create.rb +24 -34
  39. data/lib/pact_broker/client/pacticipants/describe.rb +33 -0
  40. data/lib/pact_broker/client/pacticipants/list.rb +34 -0
  41. data/lib/pact_broker/client/pacticipants/text_formatter.rb +41 -0
  42. data/lib/pact_broker/client/string_refinements.rb +56 -0
  43. data/lib/pact_broker/client/version.rb +1 -1
  44. data/lib/pact_broker/client/versions.rb +4 -1
  45. data/lib/pact_broker/client/versions/describe.rb +3 -1
  46. data/lib/pact_broker/client/versions/formatter.rb +3 -1
  47. data/lib/pact_broker/client/versions/json_formatter.rb +5 -3
  48. data/lib/pact_broker/client/versions/text_formatter.rb +3 -1
  49. data/lib/pact_broker/client/webhooks/create.rb +14 -8
  50. data/pact-broker-client.gemspec +1 -0
  51. data/script/record-deployments-and-releases.sh +18 -0
  52. data/script/webhook-commands.sh +12 -0
  53. data/spec/fixtures/approvals/can_i_deploy_failure_dry_run.approved.txt +7 -0
  54. data/spec/fixtures/approvals/can_i_deploy_success_dry_run.approved.txt +7 -0
  55. data/spec/fixtures/approvals/describe_environment.approved.txt +7 -0
  56. data/spec/fixtures/approvals/describe_pacticipant.approved.txt +2 -0
  57. data/spec/fixtures/approvals/list_environments.approved.txt +3 -0
  58. data/spec/integration/describe_environment_spec.rb +31 -0
  59. data/spec/lib/pact_broker/client/can_i_deploy_spec.rb +62 -2
  60. data/spec/lib/pact_broker/client/cli/broker_can_i_deploy_spec.rb +17 -4
  61. data/spec/lib/pact_broker/client/cli/broker_publish_spec.rb +1 -1
  62. data/spec/lib/pact_broker/client/cli/broker_run_webhook_commands_spec.rb +7 -5
  63. data/spec/lib/pact_broker/client/deployments/record_deployment_spec.rb +204 -0
  64. data/spec/lib/pact_broker/client/deployments/record_support_ended_spec.rb +208 -0
  65. data/spec/lib/pact_broker/client/deployments/record_undeployment_spec.rb +219 -0
  66. data/spec/lib/pact_broker/client/environments/delete_environment_spec.rb +120 -0
  67. data/spec/lib/pact_broker/client/environments/describe_environment_spec.rb +89 -0
  68. data/spec/lib/pact_broker/client/environments/update_environment_spec.rb +167 -0
  69. data/spec/lib/pact_broker/client/generate_display_name_spec.rb +39 -0
  70. data/spec/lib/pact_broker/client/hal/entity_spec.rb +2 -2
  71. data/spec/lib/pact_broker/client/pacticipants/create_spec.rb +2 -2
  72. data/spec/pacts/pact_broker_client-pact_broker.json +88 -108
  73. data/spec/pacts/pact_broker_client-pactflow.json +118 -0
  74. data/spec/service_providers/create_environment_spec.rb +78 -0
  75. data/spec/service_providers/list_environments_spec.rb +77 -0
  76. data/spec/service_providers/pact_broker_client_matrix_ignore_spec.rb +1 -1
  77. data/spec/service_providers/pact_broker_client_register_repository_spec.rb +2 -2
  78. data/spec/service_providers/pact_helper.rb +15 -10
  79. data/spec/service_providers/pactflow_webhooks_create_spec.rb +86 -0
  80. data/spec/service_providers/pacticipants_create_spec.rb +5 -4
  81. data/spec/service_providers/publish_pacts_spec.rb +3 -1
  82. data/spec/service_providers/record_deployment_spec.rb +14 -32
  83. data/spec/service_providers/record_release_spec.rb +132 -0
  84. data/spec/service_providers/record_undeployment_spec.rb +166 -0
  85. data/spec/service_providers/webhooks_create_spec.rb +1 -1
  86. data/spec/spec_helper.rb +15 -2
  87. data/spec/support/approvals.rb +1 -1
  88. data/spec/support/shared_context.rb +2 -1
  89. data/tasks/pact.rake +21 -1
  90. metadata +82 -7
  91. data/lib/pact_broker/client/versions/record_deployment.rb +0 -109
  92. data/lib/pact_broker/client/versions/record_undeployment.rb +0 -102
  93. data/spec/lib/pact_broker/client/versions/record_deployment_spec.rb +0 -82
@@ -0,0 +1,39 @@
1
+ require 'pact_broker/client/generate_display_name'
2
+
3
+ module PactBroker
4
+ module Client
5
+ describe GenerateDisplayName do
6
+ describe ".call" do
7
+ TEST_CASES = {
8
+ "foo" => "Foo",
9
+ "MyService" => "My Service",
10
+ "my-service" => "My Service",
11
+ "my_service" => "My Service",
12
+ "my service" => "My Service",
13
+ "ABCService" => "ABC Service",
14
+ "A4Service" => "A4 Service",
15
+ "SNSPactEventConsumer" => "SNS Pact Event Consumer",
16
+ "AWSSummiteerWeb" => "AWS Summiteer Web",
17
+ "Beer-Consumer" => "Beer Consumer",
18
+ "foo.pretend-consumer" => "Foo Pretend Consumer",
19
+ "Client-XX" => "Client XX",
20
+ "providerJSWorkshop" => "Provider JS Workshop",
21
+ "e2e Provider Example" => "E2e Provider Example",
22
+ "MP - Our Provider" => "MP - Our Provider",
23
+ "PoC - Pact-broker-consumer" => "PoC - Pact Broker Consumer",
24
+ "QB-DATABASE Service" => "QB DATABASE Service",
25
+ "Support Species App (Provider)" => "Support Species App (Provider)",
26
+ 9 => "9",
27
+ "" => "",
28
+ nil => nil
29
+ }
30
+
31
+ TEST_CASES.each do | name, expected_display_name |
32
+ it "converts #{name.inspect} to #{expected_display_name.inspect}" do
33
+ expect(GenerateDisplayName.call(name)).to eq expected_display_name
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -73,7 +73,7 @@ module PactBroker::Client
73
73
  subject(:entity) { ErrorEntity.new("http://pact", pact_hash, http_client) }
74
74
 
75
75
  it "raises an error" do
76
- expect { entity.assert_success! }.to raise_error ErrorResponseReturned, "Error retrieving http://pact status="
76
+ expect { entity.assert_success! }.to raise_error ErrorResponseReturned, "Error making request to http://pact status="
77
77
  end
78
78
  end
79
79
 
@@ -83,7 +83,7 @@ module PactBroker::Client
83
83
  subject(:entity) { ErrorEntity.new("http://pact", pact_hash, http_client, response) }
84
84
 
85
85
  it "raises an error" do
86
- expect { entity.assert_success! }.to raise_error ErrorResponseReturned, "Error retrieving http://pact status=200 body"
86
+ expect { entity.assert_success! }.to raise_error ErrorResponseReturned, "Error making request to http://pact status=200 body"
87
87
  end
88
88
  end
89
89
  end
@@ -8,11 +8,11 @@ module PactBroker
8
8
  before do
9
9
  allow_any_instance_of(PactBroker::Client::Hal::HttpClient).to receive(:sleep)
10
10
  end
11
- let(:pact_broker_client_options) { {} }
11
+ let(:pact_broker_client_options) { { pact_broker_base_url: broker_base_url} }
12
12
  let(:broker_base_url) { "http://url" }
13
13
  let(:params) { { name: 'Foo' } }
14
14
 
15
- subject { Create.call(params, broker_base_url, pact_broker_client_options)}
15
+ subject { Create.call(params, {}, pact_broker_client_options)}
16
16
 
17
17
  context "when there is an http error" do
18
18
  let!(:index_request) do
@@ -292,6 +292,94 @@
292
292
  }
293
293
  }
294
294
  },
295
+ {
296
+ "description": "a request to determine if Bar can be deployed with all Foo tagged prod, ignoring the verification for Foo version 3.4.5",
297
+ "providerState": "provider Bar version 4.5.6 has a successful verification for Foo version 1.2.3 tagged prod and a failed verification for version 3.4.5 tagged prod",
298
+ "request": {
299
+ "method": "get",
300
+ "path": "/matrix",
301
+ "query": "q%5B%5D%5Bpacticipant%5D=Bar&q%5B%5D%5Bversion%5D=4.5.6&q%5B%5D%5Bpacticipant%5D=Foo&q%5B%5D%5Btag%5D=prod&latestby=cvpv&ignore%5B%5D%5Bpacticipant%5D=Foo&ignore%5B%5D%5Bversion%5D=3.4.5"
302
+ },
303
+ "response": {
304
+ "status": 200,
305
+ "headers": {
306
+ "Content-Type": "application/hal+json;charset=utf-8"
307
+ },
308
+ "body": {
309
+ "summary": {
310
+ "deployable": true,
311
+ "ignored": 1
312
+ },
313
+ "notices": [
314
+ {
315
+ "text": "some notice",
316
+ "type": "info"
317
+ }
318
+ ],
319
+ "matrix": [
320
+ {
321
+ "consumer": {
322
+ "name": "Foo",
323
+ "version": {
324
+ "number": "1.2.3"
325
+ }
326
+ },
327
+ "provider": {
328
+ "name": "Bar",
329
+ "version": {
330
+ "number": "4.5.6"
331
+ }
332
+ },
333
+ "verificationResult": {
334
+ "success": true,
335
+ "_links": {
336
+ "self": {
337
+ "href": "http://result"
338
+ }
339
+ }
340
+ }
341
+ },
342
+ {
343
+ "consumer": {
344
+ "name": "Foo",
345
+ "version": {
346
+ "number": "3.4.5"
347
+ }
348
+ },
349
+ "provider": {
350
+ "name": "Bar",
351
+ "version": {
352
+ "number": "4.5.6"
353
+ }
354
+ },
355
+ "verificationResult": {
356
+ "success": false,
357
+ "_links": {
358
+ "self": {
359
+ "href": "http://result"
360
+ }
361
+ }
362
+ },
363
+ "ignored": true
364
+ }
365
+ ]
366
+ },
367
+ "matchingRules": {
368
+ "$.body.notices": {
369
+ "min": 1
370
+ },
371
+ "$.body.notices[*].*": {
372
+ "match": "type"
373
+ },
374
+ "$.body.matrix[0].verificationResult._links.self.href": {
375
+ "match": "type"
376
+ },
377
+ "$.body.matrix[1].verificationResult._links.self.href": {
378
+ "match": "type"
379
+ }
380
+ }
381
+ }
382
+ },
295
383
  {
296
384
  "description": "a request for the compatibility matrix for Foo version 1.2.3 and Bar version 4.5.6",
297
385
  "providerState": "the pact for Foo version 1.2.3 has been verified by Bar version 4.5.6",
@@ -1550,113 +1638,6 @@
1550
1638
  }
1551
1639
  }
1552
1640
  },
1553
- {
1554
- "description": "a request for the index resource",
1555
- "providerState": "the pb:publish-contracts relations exists in the index resource",
1556
- "request": {
1557
- "method": "GET",
1558
- "path": "/",
1559
- "headers": {
1560
- "Accept": "application/hal+json"
1561
- }
1562
- },
1563
- "response": {
1564
- "status": 200,
1565
- "headers": {
1566
- "Content-Type": "application/hal+json;charset=utf-8"
1567
- },
1568
- "body": {
1569
- "_links": {
1570
- "pb:publish-contracts": {
1571
- "href": "http://localhost:1234/HAL-REL-PLACEHOLDER-PB-PUBLISH-CONTRACTS"
1572
- }
1573
- }
1574
- },
1575
- "matchingRules": {
1576
- "$.body._links.pb:publish-contracts.href": {
1577
- "match": "regex",
1578
- "regex": "http:\\/\\/.*"
1579
- }
1580
- }
1581
- }
1582
- },
1583
- {
1584
- "description": "a request to publish contracts",
1585
- "request": {
1586
- "method": "POST",
1587
- "path": "/HAL-REL-PLACEHOLDER-PB-PUBLISH-CONTRACTS",
1588
- "headers": {
1589
- "Content-Type": "application/json",
1590
- "Accept": "application/hal+json"
1591
- },
1592
- "body": {
1593
- "pacticipantName": "Foo",
1594
- "pacticipantVersionNumber": "5556b8149bf8bac76bc30f50a8a2dd4c22c85f30",
1595
- "branch": "main",
1596
- "tags": [
1597
- "dev"
1598
- ],
1599
- "buildUrl": "http://build",
1600
- "contracts": [
1601
- {
1602
- "consumerName": "Foo",
1603
- "providerName": "Bar",
1604
- "specification": "pact",
1605
- "contentType": "application/json",
1606
- "content": "eyJjb25zdW1lciI6eyJuYW1lIjoiRm9vIn0sInByb3ZpZGVyIjp7Im5hbWUiOiJCYXIifSwiaW50ZXJhY3Rpb25zIjpbeyJkZXNjcmlwdGlvbiI6ImFuIGV4YW1wbGUgcmVxdWVzdCIsInByb3ZpZGVyU3RhdGUiOiJhIHByb3ZpZGVyIHN0YXRlIiwicmVxdWVzdCI6eyJtZXRob2QiOiJHRVQiLCJwYXRoIjoiLyIsImhlYWRlcnMiOnt9fSwicmVzcG9uc2UiOnsic3RhdHVzIjoyMDAsImhlYWRlcnMiOnsiQ29udGVudC1UeXBlIjoiYXBwbGljYXRpb24vaGFsK2pzb24ifX19XSwibWV0YWRhdGEiOnsicGFjdFNwZWNpZmljYXRpb24iOnsidmVyc2lvbiI6IjIuMC4wIn19fQ==",
1607
- "writeMode": "overwrite",
1608
- "onConflict": "overwrite"
1609
- }
1610
- ]
1611
- }
1612
- },
1613
- "response": {
1614
- "status": 200,
1615
- "headers": {
1616
- "Content-Type": "application/hal+json;charset=utf-8"
1617
- },
1618
- "body": {
1619
- "_embedded": {
1620
- "pacticipant": {
1621
- "name": "Foo"
1622
- },
1623
- "version": {
1624
- "number": "5556b8149bf8bac76bc30f50a8a2dd4c22c85f30",
1625
- "buildUrl": "http://build"
1626
- }
1627
- },
1628
- "logs": [
1629
- {
1630
- "level": "info",
1631
- "message": "some message"
1632
- }
1633
- ],
1634
- "_links": {
1635
- "pb:pacticipant-version-tags": [
1636
- {
1637
- "name": "dev"
1638
- }
1639
- ],
1640
- "pb:contracts": [
1641
- {
1642
- "href": "http://some-pact"
1643
- }
1644
- ]
1645
- }
1646
- },
1647
- "matchingRules": {
1648
- "$.body.logs": {
1649
- "min": 1
1650
- },
1651
- "$.body.logs[*].*": {
1652
- "match": "type"
1653
- },
1654
- "$.body._links.pb:contracts[0].href": {
1655
- "match": "type"
1656
- }
1657
- }
1658
- }
1659
- },
1660
1641
  {
1661
1642
  "description": "a request to create a webhook with a JSON body for a consumer and provider",
1662
1643
  "providerState": "the 'Pricing Service' and 'Condor' already exist in the pact-broker",
@@ -1865,7 +1846,6 @@
1865
1846
  }
1866
1847
  ],
1867
1848
  "request": {
1868
- "url": null,
1869
1849
  "method": "POST",
1870
1850
  "headers": {
1871
1851
  "Foo": "bar",
@@ -0,0 +1,118 @@
1
+ {
2
+ "consumer": {
3
+ "name": "Pact Broker Client"
4
+ },
5
+ "provider": {
6
+ "name": "Pactflow"
7
+ },
8
+ "interactions": [
9
+ {
10
+ "description": "a request for the index resource",
11
+ "request": {
12
+ "method": "get",
13
+ "path": "/",
14
+ "headers": {
15
+ "Accept": "application/hal+json"
16
+ }
17
+ },
18
+ "response": {
19
+ "status": 200,
20
+ "headers": {
21
+ "Content-Type": "application/hal+json;charset=utf-8"
22
+ },
23
+ "body": {
24
+ "_links": {
25
+ "pb:webhooks": {
26
+ "href": "http://localhost:1235/HAL-REL-PLACEHOLDER-PB-WEBHOOKS"
27
+ },
28
+ "pb:pacticipants": {
29
+ "href": "http://localhost:1235/HAL-REL-PLACEHOLDER-PB-PACTICIPANTS"
30
+ },
31
+ "pb:pacticipant": {
32
+ "href": "http://localhost:1235/HAL-REL-PLACEHOLDER-PB-PACTICIPANT-{pacticipant}"
33
+ }
34
+ }
35
+ },
36
+ "matchingRules": {
37
+ "$.body._links.pb:webhooks.href": {
38
+ "match": "regex",
39
+ "regex": "http:\\/\\/.*"
40
+ },
41
+ "$.body._links.pb:pacticipants.href": {
42
+ "match": "regex",
43
+ "regex": "http:\\/\\/.*"
44
+ },
45
+ "$.body._links.pb:pacticipant.href": {
46
+ "match": "regex",
47
+ "regex": "http:\\/\\/.*{pacticipant}"
48
+ }
49
+ }
50
+ }
51
+ },
52
+ {
53
+ "description": "a request to create a webhook for a team",
54
+ "providerState": "a team with UUID 2abbc12a-427d-432a-a521-c870af1739d9 exists",
55
+ "request": {
56
+ "method": "post",
57
+ "path": "/HAL-REL-PLACEHOLDER-PB-WEBHOOKS",
58
+ "headers": {
59
+ "Content-Type": "application/json",
60
+ "Accept": "application/hal+json"
61
+ },
62
+ "body": {
63
+ "description": "a webhook",
64
+ "events": [
65
+ {
66
+ "name": "contract_content_changed"
67
+ }
68
+ ],
69
+ "request": {
70
+ "url": "https://webhook",
71
+ "method": "POST",
72
+ "headers": {
73
+ "Foo": "bar",
74
+ "Bar": "foo"
75
+ },
76
+ "body": {
77
+ "some": "body"
78
+ }
79
+ },
80
+ "teamUuid": "2abbc12a-427d-432a-a521-c870af1739d9"
81
+ }
82
+ },
83
+ "response": {
84
+ "status": 201,
85
+ "headers": {
86
+ "Content-Type": "application/hal+json;charset=utf-8"
87
+ },
88
+ "body": {
89
+ "description": "a webhook",
90
+ "teamUuid": "2abbc12a-427d-432a-a521-c870af1739d9",
91
+ "_links": {
92
+ "self": {
93
+ "href": "http://localhost:1234/some-url",
94
+ "title": "A title"
95
+ }
96
+ }
97
+ },
98
+ "matchingRules": {
99
+ "$.body.description": {
100
+ "match": "type"
101
+ },
102
+ "$.body._links.self.href": {
103
+ "match": "regex",
104
+ "regex": "http:\\/\\/.*"
105
+ },
106
+ "$.body._links.self.title": {
107
+ "match": "type"
108
+ }
109
+ }
110
+ }
111
+ }
112
+ ],
113
+ "metadata": {
114
+ "pactSpecification": {
115
+ "version": "2.0.0"
116
+ }
117
+ }
118
+ }
@@ -0,0 +1,78 @@
1
+ require 'service_providers/pact_helper'
2
+ require 'pact_broker/client/environments/create_environment'
3
+
4
+ RSpec.describe "create an environment", pact: true, skip: true do
5
+ include_context "pact broker"
6
+ include PactBrokerPactHelperMethods
7
+
8
+ let(:params) do
9
+ {
10
+ name: "test",
11
+ display_name: "Test",
12
+ production: false,
13
+ contact_name: "Foo team",
14
+ contact_email_address: "foo@bar.com"
15
+
16
+ }
17
+ end
18
+ let(:pact_broker_client_options) { {} }
19
+ let(:request_body) do
20
+ {
21
+ name: "test",
22
+ displayName: "Test",
23
+ production: false,
24
+ contacts: [{
25
+ name: "Foo team",
26
+ details: {
27
+ emailAddress: "foo@bar.com"
28
+ }
29
+ }]
30
+ }
31
+ end
32
+
33
+ subject { PactBroker::Client::Environments::CreateEnvironment.call(params, broker_base_url, pact_broker_client_options) }
34
+
35
+ def mock_index
36
+ pact_broker
37
+ .given("the pb:environments relation exists in the index resource")
38
+ .upon_receiving("a request for the index resource")
39
+ .with(
40
+ method: "GET",
41
+ path: '/',
42
+ headers: get_request_headers).
43
+ will_respond_with(
44
+ status: 200,
45
+ headers: pact_broker_response_headers,
46
+ body: {
47
+ _links: {
48
+ :'pb:environments' => {
49
+ href: placeholder_url_term("pb:environments")
50
+ }
51
+ }
52
+ }
53
+ )
54
+ end
55
+
56
+ def mock_environment_creation_request
57
+ pact_broker
58
+ .upon_receiving("a request to create an environment")
59
+ .with(
60
+ method: "POST",
61
+ path: "/HAL-REL-PLACEHOLDER-PB-ENVIRONMENTS",
62
+ headers: post_request_headers,
63
+ body: request_body
64
+ )
65
+ .will_respond_with(
66
+ status: 201,
67
+ headers: pact_broker_response_headers,
68
+ body: request_body.merge("uuid" => Pact.like("ffe683ef-dcd7-4e4f-877d-f6eb3db8e86e"))
69
+ )
70
+ end
71
+
72
+ it "returns a success result" do
73
+ mock_index
74
+ mock_environment_creation_request
75
+ expect(subject.success).to be true
76
+ expect(subject.message).to include "Created test environment in the Pact Broker with UUID ffe683ef-dcd7-4e4f-877d-f6eb3db8e86e"
77
+ end
78
+ end