├── .gitignore
├── .travis.yml
├── LICENSE.md
├── README.md
├── composer.json
├── docs
├── .nojekyll
├── README.md
├── assets
│ ├── forge_deployment_webhook.png
│ ├── laravel-sanity-banner-v2.png
│ └── laravel-sanity-banner.png
└── index.html
├── phpunit.xml
├── src
├── Badges.php
├── Commands
│ ├── SanityMock.php
│ └── SanityReset.php
├── Config
│ ├── config.php
│ └── phpcs.xml
├── Events
│ └── RunnerEvent.php
├── Factory.php
├── Routes
│ └── routes.php
├── Runners
│ ├── DuskTestRunner.php
│ ├── Runner.php
│ ├── RunnerForMiniGame.php
│ ├── ScoreboardRunner.php
│ ├── StyleTestRunner.php
│ └── UnitTestRunner.php
├── SanityServiceProvider.php
├── SlackMessage.php
└── Subscriber.php
└── tests
├── .gitkeep
├── TestCase.php
└── Unit
└── SanityTest.php
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | sudo: true
4 |
5 | php:
6 | - 7.2
7 |
8 | services:
9 | - sqlite
10 |
11 | before_script:
12 | - composer install
13 | - travis_retry composer self-update
14 | - travis_retry composer update --no-interaction --prefer-dist
15 | - composer show laravel/framework
16 | - composer dumpautoload
17 |
18 | script:
19 | - vendor/bin/phpunit
20 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Stephen Lake
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Laravel Sanity
2 |
3 | 
4 | 
5 | 
6 | 
7 | 
8 |
9 | **Laravel Sanity** is a **staging** package to assist in running your own unit testing, code standards and custom runner automation on your staging/testing environment using any deployment service that allows post-deployment webhook triggers. Ideal for start-ups or teams that prefer a simple testing environment over enterprise CI. Keep track of breakage scores and compete with your team in keeping your codebase clean!
10 |
11 | Made with ❤️ by [Stephen Lake](http://github.com/stephenlake)
12 |
13 | ## Getting Started
14 |
15 | Install the package via composer.
16 |
17 | composer require cloudcake/laravel-sanity
18 |
19 | #### See [documentation](https://stephenlake.github.io/laravel-sanity/) for full installation and getting started.
20 |
21 | ## License
22 |
23 | This library is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details.
24 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cloudcake/laravel-sanity",
3 | "description": "Automate builds and code standards sniffing. Now with badges.",
4 | "keywords": ["automate", "ci", "testing", "badges"],
5 | "license": "MIT",
6 | "type": "library",
7 | "authors": [{
8 | "name": "Stephen Lake",
9 | "email": "stephen@closurecode.com"
10 | }],
11 | "require": {
12 | "phpunit/phpunit": "~7.0|~8.0",
13 | "squizlabs/php_codesniffer": "^3.4",
14 | "kitetail/zttp": "^0.4.0"
15 | },
16 | "require-dev": {
17 | "laravel/framework": "~5.5.0|~5.6.0|~5.7.0|~5.8.0",
18 | "orchestra/testbench": "~3.4.0|~3.5.0|~3.6.0"
19 | },
20 | "autoload": {
21 | "psr-4": {
22 | "Sanity\\": "src/"
23 | }
24 | },
25 | "autoload-dev": {
26 | "psr-4": {
27 | "Sanity\\Tests\\": "tests/"
28 | }
29 | },
30 | "extra": {
31 | "laravel": {
32 | "providers": [
33 | "Sanity\\SanityServiceProvider"
34 | ]
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/docs/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cloudcake/laravel-sanity/5b60c67e206ded0f82343070918beb615d1d08e6/docs/.nojekyll
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Self hosted in-app testing, coding standards and custom runner automation for Laravel. Now with badges!
7 |
8 |
9 | # Getting Started
10 |
11 | ## Install the package via composer
12 |
13 | composer require stephenlake/laravel-sanity --dev
14 |
15 | ## Register the service provider
16 |
17 | This package makes use of Laravel's auto-discovery. If you are an using earlier version of Laravel (< 5.4) you will need to manually register the service provider:
18 |
19 | Add `Sanity\SanityServiceProvider::class` to the `providers` array in `config/app.php`.
20 |
21 | ## Publish configuration
22 |
23 | `php artisan vendor:publish --provider="Sanity\SanityServiceProvider" --tag="config"`
24 |
25 | This will publish `sanity.php` to your `config/` path and a `phpcs.xml` config in your base app path.
26 |
27 | ## Install Sanity's Routes
28 |
29 | Generally packages would load their routes automatically, however Sanity allows you to customise your routes in your `config/sanity.php` config and wrap them in groups if you like.
30 |
31 | Open up your routes file (default: `routes/web.php`) and add:
32 | `\Sanity\Factory::routes();`
33 |
34 | This will load the configured routes (in `config/sanity.php`).
35 |
36 | ## Configure Your Deployment Service
37 |
38 | It's important to keep in mind that this package is intended to be run on your staging/testing servers. While you may run it on production, it's highly discouraged as tests are run **after** the code has already been deployed.
39 |
40 | The Sanity webhook is the URL that must be hit to trigger Sanity to process its tests. Enter your full domain and the configured webhook endpoint into your deployment configuration:
41 |
42 | **Laravel Forge Example:**
43 |
44 | 
45 |
46 | Where `example.org` is your applications **staging/testing** domain name and `/sanity/forge` is the endpoint you have configured in `config/sanity.php` under the `webhook` field.
47 |
48 | # Usage
49 |
50 | ## Runners
51 |
52 | Runners are individual classes used to perform tests (and other tasks) on your code base and store the result with their state at the time of the latest commit. Runners store information like the committer, the state before and after the committer triggered the runner and whether they broke or fixed your code.
53 |
54 | ### Prepackaged Runners
55 |
56 | Sanity is packaged with some useful predefined runners out of the box to express its awesomeness.
57 |
58 | #### Unit Runner
59 |
60 | [(`\Sanity\Runners\UnitTestRunner`)](https://github.com/stephenlake/laravel-sanity/blob/master/src/Runners/UnitTestRunner.php)
61 |
62 | The unit runner runs your configured PHPUnit tests.
63 |
64 | #### Dusk Runner
65 |
66 | [(`\Sanity\Runners\DuskTestRunner`)](https://github.com/stephenlake/laravel-sanity/blob/master/src/Runners/DuskTestRunner.php)
67 |
68 | The dusk runner runs your configured Laravel Dusk tests.
69 |
70 | #### Style Runner
71 |
72 | [(`\Sanity\Runners\StyleTestRunner`)](https://github.com/stephenlake/laravel-sanity/blob/master/src/Runners/StyleTestRunner.php)
73 |
74 | The style runner performs a strict set of PSR (with some customizations) rules to ensure that your code format and documentation is top-notch.
75 |
76 | #### Scoreboard Runner
77 |
78 | [(`\Sanity\Runners\ScoreboardRunner`)](https://github.com/stephenlake/laravel-sanity/blob/master/src/Runners/ScoreboardRunner.php)
79 |
80 | The scoreboard runner applies a points system to each pusher. When a pusher fixes, breaks or leaves other runners in the same state they were before their push, an allocated number of points (could be negative) will be associated to that user.
81 |
82 |
83 | ### Creating Custom Runners
84 |
85 | Creating your own runners couldn't be easier. Simply create a class that extends the Sanity base runner, give it a name and provide a run method that ends with telling Sanity whether your runner succeeded or failed. Let's run through an example.
86 |
87 | We'll create a runner that tests if the `storage` directory is writable.
88 |
89 | #### Create your runner class
90 |
91 | markAsPassed();
105 | } else {
106 | $this->markAsFailed();
107 | }
108 | }
109 | }
110 |
111 | #### Add your runner to the config
112 |
113 | 'runners' => [
114 | Sanity\Runners\UnitTestRunner::class,
115 | Sanity\Runners\DuskTestRunner::class,
116 | Sanity\Runners\StyleTestRunner::class,
117 | Sanity\Runners\ScoreboardRunner::class,
118 | App\WritableStorageTestRunner::class
119 | ],
120 |
121 | #### Test your runner
122 |
123 | Sanity comes bundled with a test command (`\Sanity\Commands\SanityMock`) which may be used to simulate a deployment payload from Laravel Forge. Run this command to test your new runner:
124 |
125 | `php artisan sanity:mock`
126 |
127 | It's that simple. And you magically have a badge to display, check it out by opening your configured badges endpoint, defaults to `http://localhost/sanity/badges/writable-storage.svg`!
128 |
129 | 
130 |
131 | ### Available Attributes Options
132 |
133 | The base Sanity runner class contains a number of proctected attributes to customize your runners. See the complete list below:
134 |
135 | #### `name`
136 |
137 | `protected $name` `string` default: `Runner`
138 |
139 | A unique name for the runner. Must be changed. Used for mapping runners and must be unique.
140 |
141 | #### `badgeLabel`
142 |
143 | `protected $badgeLabel` `string` default: `Runner`
144 |
145 | The label to display on the generated badge.
146 |
147 | #### `badgeColourPassing`
148 |
149 | `protected $badgeColourPassing` `string` default: `99cc00`
150 |
151 | The colour of the badge when passing. The value should be a hex value **without the leading hash (#)**.
152 |
153 | #### `badgeColourFailing`
154 |
155 | `protected $badgeColourFailing` `string` default: `c53232`
156 |
157 | The colour of the badge when failing. The value should be a hex value **without the leading hash (#)**.
158 |
159 | #### `badgeColourUnknown`
160 |
161 | `protected $badgeColourUnknown` `string` default: `989898`
162 |
163 | The colour of the badge when pending. The value should be a hex value **without the leading hash (#)**.
164 |
165 | #### `badgeValuePassing`
166 |
167 | `protected $badgeValuePassing` `string` default: `passing`
168 |
169 | The text to display when the runner is passing.
170 |
171 | #### `badgeValueFailing`
172 |
173 | `protected $badgeValueFailing` `string` default: `failing`
174 |
175 | The text to display when the runner is failing.
176 |
177 | #### `badgeValueUnknown`
178 |
179 | `protected $badgeValueUnknown` `string` default: `pending`
180 |
181 | The text to display when the runner hasn't run or is pending.
182 |
183 | #### `shouldFireEvents`
184 |
185 | `protected $shouldFireEvents` `boolean` default: `true`
186 |
187 | Boolean value indicated whether or not the runner should fire success and failure events once it has been run.
188 |
189 | #### `collectsStats`
190 |
191 | `protected $collectsStats` `boolean` default: `false`
192 |
193 | If set to true, the runner will run after all other runners that are not set to collect stats. This is useful when you need your runner to be executed after everything else has been run in order to collect the results of the other runners.
194 |
195 | ### Available Runner Methods
196 |
197 | Runners' helper methods:
198 |
199 | #### `markAsPassed()`
200 |
201 | Mark the runner as a success.
202 |
203 | #### `markAsFailed()`
204 |
205 | Mark the runner as a failure.
206 |
207 | #### `passing()`
208 |
209 | Returns true if the runner has passed.
210 |
211 | #### `failing()`
212 |
213 | Returns true if the runner has failed.
214 |
215 | #### `isCurrentlyPassing()`
216 |
217 | Alias of `passing()`. Returns true if the runner has passed.
218 |
219 | #### `isCurrentlyFailing()`
220 |
221 | Alias of `failing()`. Returns true if the runner has failed.
222 |
223 | #### `hasntRun()`
224 |
225 | Returns true if the runner hasn't run yet.
226 |
227 | #### `setResults(array $results)`
228 |
229 | Stores logs in array format for the recorded runner. This is required if you wish to preset logs in your notifications.
230 |
231 | #### `getCommit()`
232 |
233 | Get the latest commit information from the push that triggered the runner to execute.
234 |
235 | #### `getResults()`
236 |
237 | Get stored logs from the runner as an array.
238 |
239 | #### `wasJustBroken()`
240 |
241 | Returns true if this runner was previously successful, but currently failing.
242 |
243 | #### `wasJustFixed()`
244 |
245 | Returns true if this runner was previously failing, but currently passing.
246 |
247 | #### `collectsStats()`
248 |
249 | Returns true if the runner collects stats.
250 |
251 | ## Listening for results
252 |
253 | The configuration file contains a `subscriber` field which if undefined points to a default subscriber that listens for events. If you would like your app to listen to these events and fire off your own notifications, you may do so by creating your own subscriber class and extending `Sanity\Subscriber`.
254 |
255 | ### Create your subscriber class
256 |
257 | Create a `SanityEventSubscriber.php` file in `app/` with content:
258 |
259 | `App\SanityEventSubscriber::class`
302 |
303 | ## Slack notifications
304 |
305 | The base subscriber is bundled with a little Slack helper to assist in submitting Slack notifications via webhook URL.
306 |
307 | getCommit();
320 | $pusher = $commit['commit_author'];
321 |
322 | if ($runner->wasJustFixed()) {
323 | slack(self::MY_SLACK_WEHBOOK_URL)
324 | ->success()
325 | ->title("{$pusher} fixed the tests!")
326 | ->text("{$pusher} committed and fixed broken tests!")
327 | ->send();
328 | } else {
329 | slack(self::MY_SLACK_WEHBOOK_URL)
330 | ->success()
331 | ->title("The tests are still passing!")
332 | ->text("{$pusher} committed without breaking the test!")
333 | ->send();
334 | }
335 | }
336 |
337 | public function onMyRunnerFailure($runner)
338 | {
339 | if ($runner->wasJustBroken()) {
340 | slack(self::MY_SLACK_WEHBOOK_URL)
341 | ->danger()
342 | ->title("{$pusher} broke the tests!")
343 | ->text("{$pusher} committed and broke a passing test!")
344 | ->send();
345 | } else {
346 | slack(self::MY_SLACK_WEHBOOK_URL)
347 | ->danger()
348 | ->title("The tests are still broken!")
349 | ->text("{$pusher} committed without fixing the test!")
350 | ->send();
351 | }
352 | }
353 | }
354 |
355 | If the provider slack helpers are insufficient for your needs, you may submit raw payloads using the `raw()` method.
356 |
357 | # Extended Usage
358 |
359 | ## Adding pre-runners
360 |
361 | There may be situations where you need to run some setup before the tests commence. You may define pre-runner classes in the config within the `pre-runners` block. These classes must be instatiable and contain a public `run` method, example:
362 |
363 | Add the `\App\MyExamplePreRunner::class` file to the `pre-runners` block in `configs/sanity.php`:
364 |
365 | 'pre-runners' => [
366 | \App\MyExamplePreRunner::class,
367 | ],
368 |
369 | Create the pre-runner:
370 |
371 | [
398 | \App\MyExamplePostRunner::class,
399 | ],
400 |
401 | Create the pre-runner:
402 |
403 |
2 |
3 |
4 |
5 | Laravel Sanity
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 | ./tests/Unit/
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/Badges.php:
--------------------------------------------------------------------------------
1 | cache = Cache::store(config('sanity.cache'), 'file');
27 | }
28 |
29 | /**
30 | * Get badge for runner instance.
31 | *
32 | * @return void
33 | */
34 | public function get($runner, $queryString)
35 | {
36 | $label = $runner->getBadgeLabel();
37 | $status = $runner->getBadgeStatus();
38 | $colour = $runner->getBadgeColour();
39 |
40 | $badge = $this->getBadge($label, $status, $colour, $queryString);
41 |
42 | if ($badge) {
43 | return response($badge, 200)->header('Content-Type', 'image/svg+xml');
44 | }
45 |
46 | return abort(404);
47 | }
48 |
49 | /**
50 | * Get badge from cache otherwise create new.
51 | *
52 | * @return string
53 | */
54 | private function getBadge($label, $status, $colour, $queryString)
55 | {
56 | $keymd = md5($label.$status.$colour.$queryString);
57 | $badge = $this->cache->get("sanity.badges.{$keymd}", false);
58 |
59 | if (!$badge) {
60 | $badge = $this->getNewBadge($label, $status, $colour, $queryString);
61 | }
62 |
63 | return $badge;
64 | }
65 |
66 | /**
67 | * Fetch badge from shields.io.
68 | *
69 | * @return string|bool
70 | */
71 | private function getNewBadge($label, $status, $colour, $queryString)
72 | {
73 | $url = sprintf(self::BADGE_URL, $label, $status, $colour, $queryString);
74 |
75 | $res = Zttp::get($url);
76 |
77 | if ($res->isOk()) {
78 | $badge = $res->body();
79 | $keymd = md5($label.$status.$colour.$queryString);
80 | $this->cache->forever("sanity.badges.{$keymd}", $badge);
81 |
82 | return $badge;
83 | }
84 |
85 | return false;
86 | }
87 |
88 | /**
89 | * Get the actual badge and cache it.
90 | *
91 | * @return \Illuminate\Routing\ResponseFactory
92 | */
93 | private function badgeResponse($label, $status, $colour)
94 | {
95 | $opt = request()->getQueryString();
96 | $url = "https://img.shields.io/badge/{$label}-{$status}-{$colour}.svg?{$opt}";
97 | $md5 = md5($url);
98 | $key = "sanity.badges.{$md5}";
99 |
100 | if ($this->cache->has($key)) {
101 | $badge = $this->cache->get($key);
102 | } else {
103 | $badge = file_get_contents($url);
104 | $this->cache->forever($key, $badge);
105 | }
106 |
107 | return response($badge, 200)->header('Content-Type', 'image/svg+xml');
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/Commands/SanityMock.php:
--------------------------------------------------------------------------------
1 | forget("sanity.{$runner->getKeyName()}");
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Config/config.php:
--------------------------------------------------------------------------------
1 | ['local', 'testing'],
23 |
24 | /*
25 | |--------------------------------------------------------------------------
26 | | Webhook Route Endpoint
27 | |--------------------------------------------------------------------------
28 | |
29 | | You may define your own webhook endpoint here. This is the endpoint that
30 | | mustbe hit to trigger Sanity to execute all runners.
31 | |
32 | | It's recommended that you make this URL somewhat secretive by making it
33 | | a little more verbose than it currently is (add some long hash) to prevent
34 | | unauthorised hits.
35 | |
36 | */
37 |
38 | 'webhook' => '/sanity/build',
39 |
40 | /*
41 | |--------------------------------------------------------------------------
42 | | Badge Route Endpoint
43 | |--------------------------------------------------------------------------
44 | |
45 | | You can define your desired badge endpoint route if the default does not
46 | | suit your needs. This is the endpoint that must be hit to produce an image
47 | | badge for the given runner.
48 | |
49 | | If changing the route, you must include the {runner} placeholder which will
50 | | identify which badge to fetch.
51 | |
52 | */
53 |
54 | 'badges' => '/sanity/badges/{runner}.svg',
55 |
56 | /*
57 | |--------------------------------------------------------------------------
58 | | Results Route Endpoint
59 | |--------------------------------------------------------------------------
60 | |
61 | | You can define your desired results endpoint route if the default does not
62 | | suit your needs. This is the endpoint that provides output results of each
63 | | runner.
64 | |
65 | */
66 |
67 | 'results' => '/sanity/results/{runner}',
68 |
69 | /*
70 | |--------------------------------------------------------------------------
71 | | Enabled Runners
72 | |--------------------------------------------------------------------------
73 | |
74 | | Define which runners should be run with a boolean value. Any badges that
75 | | are called for disabled runners will return as 'not running' or the last
76 | | known state before the runner was disabled.
77 | |
78 | | See documentation for descriptions of the predefined runners.
79 | |
80 | */
81 |
82 | 'runners' => [
83 | Sanity\Runners\UnitTestRunner::class,
84 | Sanity\Runners\DuskTestRunner::class,
85 | Sanity\Runners\StyleTestRunner::class,
86 | Sanity\Runners\ScoreboardRunner::class,
87 | ],
88 |
89 | /*
90 | |--------------------------------------------------------------------------
91 | | Subscriber
92 | |--------------------------------------------------------------------------
93 | |
94 | | By default Sanity will catch its own events. Replacing the below subscriber
95 | | with your own which extends the Sanity subsriber will allow your app to
96 | | catch these all events events and handle them as you wish, perhaps with
97 | | email and/or slack notifications.
98 | |
99 | | See the Sanity documentation for the list of events to cater for:
100 | | https://github.com/stephenlake/laravel-sanity
101 | |
102 | */
103 |
104 | 'subscriber' => Sanity\Subscriber::class,
105 |
106 | /*
107 | |--------------------------------------------------------------------------
108 | | Cache
109 | |--------------------------------------------------------------------------
110 | |
111 | | The desired cache store to use when storing information on your app's
112 | | current state, ie: test results, coding standard results and commit info.
113 | |
114 | | This store must align with what's configured in your config/cache.php
115 | | configurations.
116 | |
117 | */
118 |
119 | 'cache' => 'file',
120 |
121 | /*
122 | |--------------------------------------------------------------------------
123 | | Pre-runners
124 | |--------------------------------------------------------------------------
125 | |
126 | | If there's tasks you wish to run before Sanity runs its checks, you can
127 | | define your pre-runner classes here. Each class requires a public run()
128 | | method which will be called.
129 | |
130 | */
131 |
132 | 'pre-runners' => [],
133 |
134 | /*
135 | |--------------------------------------------------------------------------
136 | | Post-runners
137 | |--------------------------------------------------------------------------
138 | |
139 | | If there's tasks you wish to run after Sanity runs its checks, you can
140 | | define your post-runner classes here. Each class requires a public run()
141 | | method which will be called.
142 | |
143 | */
144 |
145 | 'post-runners' => [],
146 |
147 | ];
148 |
--------------------------------------------------------------------------------
/src/Config/phpcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | PSR2 coding standards with minor tweaks
4 |
5 | app
6 | config
7 | public
8 | resources
9 | routes
10 | tests
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | *.php
78 |
79 |
80 | *.php
81 |
82 |
83 |
84 |
85 | */database/*
86 | */cache/*
87 | */*.js
88 | */*.css
89 | */*.xml
90 | */*.blade.php
91 | */autoload.php
92 | */storage/*
93 | */docs/*
94 | */vendor/*
95 | */migrations/*
96 | */config/*
97 | */public/index.php
98 | */*.blade.php
99 | */Middleware/*
100 | */Console/Kernel.php
101 | */Exceptions/Handler.php
102 | */Http/Kernel.php
103 | */Providers/*
104 |
105 |
--------------------------------------------------------------------------------
/src/Events/RunnerEvent.php:
--------------------------------------------------------------------------------
1 | runner = $runner;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/Factory.php:
--------------------------------------------------------------------------------
1 | setupRunners();
24 | }
25 |
26 | /**
27 | * Get package routes.
28 | *
29 | * @return mixed
30 | */
31 | public static function routes()
32 | {
33 | require __DIR__.'/Routes/routes.php';
34 | }
35 |
36 | /**
37 | * Create instantiations of valid runners.
38 | *
39 | * @throws Exception If a duplicate runner name is found.
40 | *
41 | * @return void
42 | */
43 | private function setupRunners()
44 | {
45 | $runners = config('sanity.runners', []);
46 |
47 | foreach ($runners as $runner) {
48 | if (is_subclass_of($runner, \Sanity\Runners\Runner::class)) {
49 | $runnerInstance = new $runner();
50 | $runnerKey = $runnerInstance->getKeyName();
51 |
52 | if (isset(self::$runners[$runnerKey])) {
53 | throw new \Exception("Duplicate runner name defined: {$runnerInstance->getName()}");
54 | }
55 |
56 | self::$runners[$runnerKey] = $runnerInstance;
57 | }
58 | }
59 | }
60 |
61 | /**
62 | * Run all configured tasks.
63 | *
64 | * @return void
65 | */
66 | public function runRunners($commit)
67 | {
68 | $this->checkEnvironment();
69 | $this->runPreRunners();
70 |
71 | // Stat collectors must run last
72 | self::$runners = collect(self::$runners)->sortBy(function ($runner, $s) {
73 | return $runner->collectsStats();
74 | })->all();
75 |
76 | foreach (self::$runners as $runner) {
77 | $runner->runNow($commit);
78 | }
79 |
80 | $this->runPostRunners();
81 | }
82 |
83 | /**
84 | * Tasks to run before running tests.
85 | *
86 | * @return void
87 | */
88 | public function runPreRunners()
89 | {
90 | $preRunners = config('sanity.pre-runners', []);
91 |
92 | if (count($preRunners)) {
93 | foreach ($preRunners as $runner) {
94 | if (class_exists($runner)) {
95 | (new $runner())->run($this->deployment);
96 | }
97 | }
98 | }
99 | }
100 |
101 | /**
102 | * Tasks to run after running tests.
103 | *
104 | * @return void
105 | */
106 | public function runPostRunners()
107 | {
108 | $postRunners = config('sanity.post-runners', []);
109 |
110 | if (count($postRunners)) {
111 | foreach ($postRunners as $runner) {
112 | if (class_exists($runner)) {
113 | (new $runner())->run($this->deployment);
114 | }
115 | }
116 | }
117 | }
118 |
119 | /**
120 | * Make sure we're running this on an allowed environment.
121 | *
122 | * @return void
123 | */
124 | private function checkEnvironment()
125 | {
126 | $environmentCurrent = env('APP_ENV', 'production');
127 | $environmentsAllowed = config('environments', ['local', 'testing']);
128 |
129 | if (!in_array($environmentCurrent, $environmentsAllowed, true)) {
130 | exit;
131 | }
132 | }
133 |
134 | /**
135 | * Get runner badge.
136 | *
137 | * @return \Illuminate\Routing\ResponseFactory
138 | */
139 | public function badge($runnerName, $queryString = '')
140 | {
141 | $runner = self::$runners[Str::slug($runnerName)] ?? false;
142 |
143 | if ($runner) {
144 | return \Facades\Sanity\Badges::get($runner, $queryString);
145 | }
146 |
147 | return abort(404);
148 | }
149 |
150 | /**
151 | * Get runner results.
152 | *
153 | * @param string $runnerName Name of the runner.
154 | *
155 | * @return \Illuminate\Routing\ResponseFactory
156 | */
157 | public function results($runnerName)
158 | {
159 | if (auth()->user()) {
160 | $result = false;
161 | $runner = self::$runners[Str::slug($runnerName)] ?? false;
162 |
163 | if ($runner) {
164 | $result = $this->formatResult($runner->getResults());
165 | }
166 |
167 | if ($result) {
168 | return $result;
169 | }
170 | }
171 |
172 | return abort(404);
173 | }
174 |
175 | /**
176 | * Format runner results.
177 | *
178 | * @param array $result The result array to format.
179 | *
180 | * @return array
181 | */
182 | private function formatResult($result)
183 | {
184 | if (($format = strtoupper(request()->query('format', 'array')))) {
185 | switch ($format) {
186 | case 'JSON':
187 | $result = json_encode($result);
188 | break;
189 | }
190 | }
191 |
192 | return $result;
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/src/Routes/routes.php:
--------------------------------------------------------------------------------
1 | all());
5 | });
6 |
7 | Route::get(config('sanity.badges', '/sanity/badges/{runner}.svg'), function ($runner) {
8 | return Facades\Sanity\Factory::badge($runner, request()->getQueryString());
9 | });
10 |
11 | Route::get(config('sanity.results', '/sanity/results/{runner}'), function ($runner) {
12 | return Facades\Sanity\Factory::results($runner);
13 | });
14 |
--------------------------------------------------------------------------------
/src/Runners/DuskTestRunner.php:
--------------------------------------------------------------------------------
1 | setResults($results);
41 |
42 | ($code == 0) ? $this->markAsPassed() : $this->markAsFailed();
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/Runners/Runner.php:
--------------------------------------------------------------------------------
1 | cache = Cache::store(config('sanity.cache'), 'file');
116 | $this->store = $this->cache->get("sanity.{$this->getKeyName()}", [
117 | 'state' => -1,
118 | 'previousPassing' => false,
119 | 'previousFailing' => false,
120 | 'previousCommit' => false,
121 | 'results' => [],
122 | ]);
123 | }
124 |
125 | /**
126 | * Execute runner.
127 | *
128 | * @return self
129 | */
130 | public function runNow(array $commit)
131 | {
132 | $this->commit = $commit;
133 |
134 | $this->run();
135 |
136 | if ($this->collectsStats()) {
137 | $this->markAsPassed();
138 | }
139 |
140 | $this->fireEvents();
141 | }
142 |
143 | /**
144 | * Runner execution.
145 | *
146 | * @return void
147 | */
148 | protected function run() : void
149 | {
150 | // Subclass execution.
151 | }
152 |
153 | public function getName() : string
154 | {
155 | return $this->name;
156 | }
157 |
158 | /**
159 | * Set runner as passing.
160 | *
161 | * @return self
162 | */
163 | public function markAsPassed()
164 | {
165 | if ($this->isCurrentlyFailing()) {
166 | $this->store['previousFailing'] = true;
167 | $this->store['previousPassing'] = false;
168 | } else {
169 | $this->store['previousFailing'] = false;
170 | $this->store['previousPassing'] = true;
171 | }
172 |
173 | $this->store['state'] = 1;
174 |
175 | $this->cacheState();
176 |
177 | return $this;
178 | }
179 |
180 | /**
181 | * Set runner as failed.
182 | *
183 | * @return self
184 | */
185 | public function markAsFailed()
186 | {
187 | if ($this->isCurrentlyPassing()) {
188 | $this->store['previousFailing'] = false;
189 | $this->store['previousPassing'] = true;
190 | } else {
191 | $this->store['previousFailing'] = true;
192 | $this->store['previousPassing'] = false;
193 | }
194 |
195 | $this->store['state'] = 0;
196 |
197 | $this->cacheState();
198 |
199 | return $this;
200 | }
201 |
202 | /**
203 | * Return true if runner was previously failing.
204 | *
205 | * @return bool
206 | */
207 | public function wasPreviouslyFailing()
208 | {
209 | return $this->store['previousFailing'];
210 | }
211 |
212 | /**
213 | * Return true if runner was previously passing.
214 | *
215 | * @return bool
216 | */
217 | public function wasPreviouslyPassing()
218 | {
219 | return $this->store['previousPassing'];
220 | }
221 |
222 | /**
223 | * Return true if runner is currently passing.
224 | *
225 | * @return bool
226 | */
227 | public function isCurrentlyPassing()
228 | {
229 | return $this->passing();
230 | }
231 |
232 | /**
233 | * Return true if runner is currently passing.
234 | *
235 | * @return bool
236 | */
237 | public function isCurrentlyFailing()
238 | {
239 | return $this->failing();
240 | }
241 |
242 | /**
243 | * Return true if runner was previously failing and is now passing.
244 | *
245 | * @return bool
246 | */
247 | public function wasJustFixed()
248 | {
249 | return $this->wasPreviouslyFailing() && $this->isCurrentlyPassing();
250 | }
251 |
252 | /**
253 | * Return true if runner was previously failing and is now passing.
254 | *
255 | * @return bool
256 | */
257 | public function wasJustBroken()
258 | {
259 | return $this->wasPreviouslyPassing() && $this->isCurrentlyFailing();
260 | }
261 |
262 | /**
263 | * Return true if runner is passing.
264 | *
265 | * @return bool
266 | */
267 | public function passing()
268 | {
269 | return $this->store['state'] == 1;
270 | }
271 |
272 | /**
273 | * Return true if runner is failing.
274 | *
275 | * @return bool
276 | */
277 | public function failing()
278 | {
279 | return $this->store['state'] == 0;
280 | }
281 |
282 | /**
283 | * Return true if runner has not run.
284 | *
285 | * @return bool
286 | */
287 | public function hasntRun()
288 | {
289 | return $this->store['state'] == -1;
290 | }
291 |
292 | /**
293 | * Set output results.
294 | *
295 | * @return self
296 | */
297 | public function setResults(array $results)
298 | {
299 | $this->store['results'] = $results;
300 |
301 | $this->cacheState();
302 |
303 | return $this;
304 | }
305 |
306 | /**
307 | * Get output results.
308 | *
309 | * @return array
310 | */
311 | public function getResults()
312 | {
313 | return $this->store['results'];
314 | }
315 |
316 | /**
317 | * Get the current commit information.
318 | *
319 | * @return array
320 | */
321 | public function getCommit()
322 | {
323 | if (!$this->commit) {
324 | exec('git log -1 --pretty=format:\''.
325 | '{"commit": "%H",'.
326 | '"abbreviated_commit": "%h",'.
327 | '"tree": "%T",'.
328 | '"abbreviated_tree": "%t",'.
329 | '"parent": "%P",'.
330 | '"abbreviated_parent": "%p",'.
331 | '"refs": "%D",'.
332 | '"encoding": "%e",'.
333 | '"subject": "%s",'.
334 | '"sanitized_subject_line": "%f",'.
335 | '"body": "%b",'.
336 | '"commit_notes": "",'.
337 | '"verification_flag": "%G?",'.
338 | '"signer": "%GS",'.
339 | '"signer_key": "%GK",'.
340 | '"author": {'.
341 | '"name": "%aN",'.
342 | '"email": "%aE",'.
343 | '"date": "%aD"},'.
344 | '"commiter": {"'.
345 | 'name": "%cN",'.
346 | '"email": "%cE",'.
347 | '"date": "%cD"'.
348 | '}'.
349 | '}\'', $output);
350 |
351 | $this->commit = json_decode($output[0]);
352 | }
353 |
354 | return $this->commit;
355 | }
356 |
357 | /**
358 | * Get key name.
359 | *
360 | * @return string
361 | */
362 | public function getKeyName()
363 | {
364 | if (!$this->keyName) {
365 | $this->keyName = Str::slug($this->name);
366 | }
367 |
368 | return $this->keyName;
369 | }
370 |
371 | /**
372 | * Get badge label.
373 | *
374 | * @return string
375 | */
376 | public function getBadgeColour()
377 | {
378 | if ($this->passing()) {
379 | return $this->badgeColourPassing;
380 | }
381 |
382 | if ($this->failing()) {
383 | return $this->badgeColourFailing;
384 | }
385 |
386 | return $this->badgeColourUnknown;
387 | }
388 |
389 | /**
390 | * Get badge label.
391 | *
392 | * @return string
393 | */
394 | public function getBadgeLabel()
395 | {
396 | return rawurlencode($this->badgeLabel);
397 | }
398 |
399 | /**
400 | * Get badge status.
401 | *
402 | * @return string
403 | */
404 | public function getBadgeStatus()
405 | {
406 | $status = $this->badgeValueUnknown;
407 |
408 | if ($this->passing()) {
409 | $status = $this->badgeValuePassing;
410 | } elseif ($this->failing()) {
411 | $status = $this->badgeValueFailing;
412 | }
413 |
414 | return rawurlencode($status);
415 | }
416 |
417 | /**
418 | * Return true if this runner collects stats.
419 | *
420 | * @return bool
421 | */
422 | public function collectsStats()
423 | {
424 | return $this->collectsStats;
425 | }
426 |
427 | /**
428 | * Fire runner events, if configured.
429 | *
430 | * @return self
431 | */
432 | private function fireEvents()
433 | {
434 | if ($this->shouldFireEvents) {
435 | event(new \Sanity\Events\RunnerEvent($this));
436 | }
437 | }
438 |
439 | /**
440 | * Store the current state of the runner.
441 | *
442 | * @return self
443 | */
444 | private function cacheState()
445 | {
446 | if ($this->store) {
447 | $this->cache->forever("sanity.{$this->getKeyName()}", $this->store);
448 | }
449 |
450 | return $this;
451 | }
452 | }
453 |
--------------------------------------------------------------------------------
/src/Runners/RunnerForMiniGame.php:
--------------------------------------------------------------------------------
1 | filter(function ($runner, $key) use ($runnerNames) {
29 | return in_array($runner->getName(), $runnerNames, true);
30 | })->all();
31 | }
32 |
33 | return $runners;
34 | }
35 |
36 | /**
37 | * Get the name of the pusher.
38 | *
39 | * @return string|null
40 | */
41 | public function getPusherName()
42 | {
43 | return $this->getCommit()->author->name ?? null;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Runners/ScoreboardRunner.php:
--------------------------------------------------------------------------------
1 | [
35 | 'passing' => 2,
36 | 'fixed' => 5,
37 | 'failing' => -3,
38 | 'broken' => -10,
39 | ],
40 | 'Unit' => [
41 | 'passing' => 2,
42 | 'fixed' => 5,
43 | 'failing' => -3,
44 | 'broken' => -10,
45 | ],
46 | 'Style' => [
47 | 'passing' => 2,
48 | 'fixed' => 10,
49 | 'failing' => -20,
50 | 'broken' => -25,
51 | ],
52 | ];
53 |
54 | /**
55 | * Runner execution.
56 | *
57 | * @return void
58 | */
59 | protected function run() : void
60 | {
61 | $results = $this->getResults();
62 | $runners = $this->getRealRunners(array_keys($this->pointsMap));
63 | $pusher = $this->getPusherName();
64 | $rState = '';
65 | $pCount = 0;
66 |
67 | foreach ($runners as $runner) {
68 | if ($runner->wasJustFixed()) {
69 | $rState = 'fixed';
70 | } elseif ($runner->wasJustBroken()) {
71 | $rState = 'broken';
72 | } elseif ($runner->isCurrentlyPassing()) {
73 | $rState = 'passing';
74 | } elseif ($runner->isCurrentlyFailing()) {
75 | $rState = 'failing';
76 | }
77 |
78 | $pCount = $this->pointsMap[$runner->getName()][$rState];
79 | $results = $this->updateScore($pusher, $results, $pCount);
80 | }
81 |
82 | $results = $this->updateRules($results);
83 | $results = $this->sortPlayersByPoints($results);
84 | $results = $this->createLabelForBadge($results);
85 |
86 | $this->setResults($results);
87 | }
88 |
89 | /**
90 | * Allocate points to player and return result.
91 | *
92 | * @param string $pusher The pusher name,
93 | * @param array $results The existing results set.
94 | * @param int $points The number of points to allocate.
95 | *
96 | * @return array
97 | */
98 | private function updateScore($pusher, $results, $points)
99 | {
100 | if (!isset($results['players'][$pusher])) {
101 | $results['players'][$pusher] = $points;
102 | } else {
103 | $results['players'][$pusher] += $points;
104 | }
105 |
106 | return $results;
107 | }
108 |
109 | /**
110 | * Sort players by points in descening order.
111 | *
112 | * @param array $results The existing result set.
113 | *
114 | * @return array
115 | */
116 | private function sortPlayersByPoints(array $results)
117 | {
118 | $results['players'] = collect($results['players'])->sortByDesc(function ($value, $key) {
119 | return $value;
120 | })->all();
121 |
122 | return $results;
123 | }
124 |
125 | /**
126 | * Create displayable label for runner badge.
127 | *
128 | * @param array $results The existing result set.
129 | *
130 | * @return array
131 | */
132 | private function createLabelForBadge(array $results)
133 | {
134 | $topPlayers = array_slice($results['players'], 0, 3);
135 | $topPlayersTemp = [];
136 |
137 | foreach ($topPlayers as $leader => $points) {
138 | $topPlayersTemp[] = $leader.' ('.number_format($points).')';
139 | }
140 |
141 | $results['badge'] = str_replace('-', '--', implode(', ', $topPlayersTemp));
142 |
143 | return $results;
144 | }
145 |
146 | /**
147 | * Add the point map allocation to the result.
148 | *
149 | * @param array $results The existing result set.
150 | *
151 | * @return array
152 | */
153 | private function updateRules(array $results)
154 | {
155 | $results['rules'] = $this->pointsMap;
156 |
157 | return $results;
158 | }
159 |
160 | /**
161 | * Get mini game players.
162 | *
163 | * @return array
164 | */
165 | public function getPlayers()
166 | {
167 | return $this->getResults()['players'] ?? [];
168 | }
169 |
170 | /**
171 | * Get mini game rules.
172 | *
173 | * @return array
174 | */
175 | public function getRules()
176 | {
177 | return $this->getResults()['rules'] ?? $this->pointsMap;
178 | }
179 |
180 | /**
181 | * Get badge status.
182 | *
183 | * @return string
184 | */
185 | public function getBadgeStatus()
186 | {
187 | $results = $this->getResults();
188 |
189 | return rawurlencode($results['badge'] ?? 'none');
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/src/Runners/StyleTestRunner.php:
--------------------------------------------------------------------------------
1 | setResults($results ?? []);
40 |
41 | if (
42 | isset($results['totals']['errors']) && $results['totals']['errors'] == 0 &&
43 | isset($results['totals']['warnings']) && $results['totals']['warnings'] == 0
44 | ) {
45 | $this->markAsPassed();
46 | } else {
47 | $this->markAsFailed();
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Runners/UnitTestRunner.php:
--------------------------------------------------------------------------------
1 | setResults($results);
41 |
42 | ($code == 0) ? $this->markAsPassed() : $this->markAsFailed();
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/SanityServiceProvider.php:
--------------------------------------------------------------------------------
1 | publishes([
28 | __DIR__.'/Config/config.php' => config_path('sanity.php'),
29 | ], 'config');
30 |
31 | $this->publishes([
32 | __DIR__.'/Config/phpcs.xml' => base_path('phpcs.xml'),
33 | ], 'config');
34 |
35 | if ($this->app->runningInConsole()) {
36 | $this->commands([
37 | \Sanity\Commands\SanityMock::class,
38 | \Sanity\Commands\SanityReset::class,
39 | ]);
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/SlackMessage.php:
--------------------------------------------------------------------------------
1 | [],
23 | ];
24 |
25 | /**
26 | * New instance of SlackMessage.
27 | *
28 | * @param string $webhook Webhook URL to post to.
29 | *
30 | * @return void
31 | */
32 | public function __construct($webhook)
33 | {
34 | $this->webhook = $webhook;
35 | }
36 |
37 | /**
38 | * Set a raw payload.
39 | *
40 | * @return self
41 | */
42 | public function raw(array $raw)
43 | {
44 | $this->payload = $raw;
45 |
46 | return $this;
47 | }
48 |
49 | /**
50 | * Set username.
51 | *
52 | * @return self
53 | */
54 | public function username($username)
55 | {
56 | $this->payload['username'] = $username;
57 |
58 | return $this;
59 | }
60 |
61 | /**
62 | * Set attachment title.
63 | *
64 | * @return self
65 | */
66 | public function title($title)
67 | {
68 | $this->payload['attachments'][0]['title'] = $title;
69 |
70 | return $this;
71 | }
72 |
73 | /**
74 | * Set attachment text.
75 | *
76 | * @return self
77 | */
78 | public function text($text)
79 | {
80 | $this->payload['attachments'][0]['text'] = $text;
81 |
82 | return $this;
83 | }
84 |
85 | /**
86 | * Set attachment pretext.
87 | *
88 | * @return self
89 | */
90 | public function pretext($pretext)
91 | {
92 | $this->payload['attachments'][0]['pretext'] = $pretext;
93 |
94 | return $this;
95 | }
96 |
97 | /**
98 | * Set attachment footer.
99 | *
100 | * @return self
101 | */
102 | public function footer($footer)
103 | {
104 | $this->payload['attachments'][0]['footer'] = $footer;
105 |
106 | return $this;
107 | }
108 |
109 | /**
110 | * Set attachment colour to green.
111 | *
112 | * @return self
113 | */
114 | public function success()
115 | {
116 | $this->payload['attachments'][0]['color'] = '#99cc00';
117 |
118 | return $this;
119 | }
120 |
121 | /**
122 | * Set attachment colour to red.
123 | *
124 | * @return self
125 | */
126 | public function danger()
127 | {
128 | $this->payload['attachments'][0]['color'] = '#c53232';
129 |
130 | return $this;
131 | }
132 |
133 | /**
134 | * Add attachment field.
135 | *
136 | * @return self
137 | */
138 | public function field($title, $value, $short = true)
139 | {
140 | $this->payload['attachments'][0]['fields'][] = [
141 | 'title' => $title,
142 | 'value' => $value,
143 | 'short' => $short,
144 | ];
145 |
146 | return $this;
147 | }
148 |
149 | /**
150 | * Add attachment action.
151 | *
152 | * @return self
153 | */
154 | public function action($type, $text, $url, $style = '')
155 | {
156 | $this->payload['attachments'][0]['actions'][] = [
157 | 'type' => $type,
158 | 'text' => $text,
159 | 'url' => $url,
160 | 'style'=> $style,
161 | ];
162 |
163 | return $this;
164 | }
165 |
166 | /**
167 | * Send the message.
168 | *
169 | * @return void
170 | */
171 | public function send()
172 | {
173 | $this->payload['attachments'][0]['ts'] = time();
174 |
175 | Zttp::asJson()->post($this->webhook, $this->payload);
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/src/Subscriber.php:
--------------------------------------------------------------------------------
1 | runner;
17 | $runnerState = $runnerClass->passing() ? 'Success' : 'Failure';
18 | $runnerMethod = "on{$runnerClass->getName()}{$runnerState}";
19 |
20 | if (method_exists($this, $runnerMethod)) {
21 | $this->$runnerMethod($runnerClass);
22 | }
23 | }
24 |
25 | /**
26 | * Get new instance of SlackMessage.
27 | *
28 | * @param string $webhook Webhook URL to post to.
29 | *
30 | * @return SlackMessage
31 | */
32 | public function slack($webhook)
33 | {
34 | return new SlackMessage($webhook);
35 | }
36 |
37 | /**
38 | * Register the listeners for the subscriber.
39 | *
40 | * @param \Illuminate\Events\Dispatcher $events
41 | */
42 | public function subscribe($events)
43 | {
44 | $subscriber = config('sanity.subscriber', get_class());
45 |
46 | $events->listen('Sanity\Events\RunnerEvent', "{$subscriber}@onRunnerFinished");
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/tests/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cloudcake/laravel-sanity/5b60c67e206ded0f82343070918beb615d1d08e6/tests/.gitkeep
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | set('database.default', 'testbench');
24 | $app['config']->set('database.connections.testbench', [
25 | 'driver' => 'sqlite',
26 | 'database' => ':memory:',
27 | 'prefix' => '',
28 | ]);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tests/Unit/SanityTest.php:
--------------------------------------------------------------------------------
1 | assertTrue(true);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------