├── src ├── routes │ ├── .gitkeep │ └── web.php ├── config │ ├── tddd │ │ ├── .gitkeep │ │ ├── pipers │ │ │ ├── tee.yml │ │ │ ├── script-macos.yml │ │ │ └── script-debian.yml │ │ ├── editors │ │ │ ├── sublime.yml │ │ │ ├── phpstorm.yml │ │ │ └── vscode.yml │ │ ├── testers │ │ │ ├── atoum.yml │ │ │ ├── behat.yml │ │ │ ├── phpspec.yml │ │ │ ├── phpunit.yml │ │ │ ├── tester.yml │ │ │ ├── rake.yml │ │ │ ├── simple-phpunit.yml │ │ │ ├── ava.yml │ │ │ ├── jest.yml │ │ │ ├── react-scripts.yml │ │ │ ├── codeception.yml │ │ │ └── dusk.yml │ │ ├── routes.yml │ │ ├── projects │ │ │ └── phpunit-example.yml │ │ ├── notifications.yml │ │ └── root.yml │ └── tddd-base.php ├── database │ └── migrations │ │ ├── .gitkeep │ │ ├── 2017_10_09_800008_add_sha1.php │ │ ├── 2017_10_10_800009_add_env.php │ │ ├── 2017_10_10_800010_add_editor.php │ │ ├── 2017_10_11_800011_add_sha1_index.php │ │ ├── 2017_09_30_800003_add_uses_tee.php │ │ ├── 2017_09_25_800001_create_runs_indexes.php │ │ ├── 2017_10_11_800012_add_project_enabled.php │ │ ├── 2017_10_01_800005_add_regex_pattern.php │ │ ├── 2017_10_08_800007_add_require_script.php │ │ ├── 2017_10_01_800006_add_test_path.php │ │ ├── 2017_09_30_800004_add_screenshots.php │ │ ├── 2014_11_14_300000_create_tddd_projects_table.php │ │ ├── 2017_12_07_800015_add_coverage.php │ │ ├── 2017_11_03_800014_add_pipers.php │ │ ├── 2014_11_14_200000_create_tddd_testers_table.php │ │ ├── 2014_11_14_700000_create_tddd_queue_table.php │ │ ├── 2017_09_27_800002_create_runs_start_end.php │ │ ├── 2014_11_14_800000_create_tddd_runs_table.php │ │ ├── 2014_11_14_500000_create_tddd_tests_table.php │ │ ├── 2017_10_27_800013_log_medium_text.php │ │ └── 2014_11_14_400000_create_tddd_suites_table.php ├── resources │ ├── assets │ │ ├── js │ │ │ ├── constants.js │ │ │ ├── bootstrap-app.js │ │ │ ├── app.js │ │ │ └── components │ │ │ │ ├── State.vue │ │ │ │ ├── Projects.vue │ │ │ │ └── Log.vue │ │ └── sass │ │ │ ├── _variables.scss │ │ │ └── app.scss │ ├── lang │ │ └── en │ │ │ ├── pagination.php │ │ │ ├── auth.php │ │ │ ├── passwords.php │ │ │ └── validation.php │ └── views │ │ └── dashboard.blade.php ├── package │ ├── Support │ │ ├── jasonlewis │ │ │ └── resource-watcher │ │ │ │ ├── .gitignore │ │ │ │ ├── src │ │ │ │ └── JasonLewis │ │ │ │ │ └── ResourceWatcher │ │ │ │ │ ├── Resource │ │ │ │ │ ├── ResourceInterface.php │ │ │ │ │ ├── DirectoryResource.php │ │ │ │ │ └── FileResource.php │ │ │ │ │ ├── Integration │ │ │ │ │ └── LaravelServiceProvider.php │ │ │ │ │ ├── Event.php │ │ │ │ │ ├── Tracker.php │ │ │ │ │ ├── Watcher.php │ │ │ │ │ └── Listener.php │ │ │ │ ├── composer.json │ │ │ │ ├── LICENSE │ │ │ │ └── README.md │ │ ├── Constants.php │ │ ├── helpers.php │ │ ├── Executor.php │ │ └── Notifier.php │ ├── Notifications │ │ ├── Channels │ │ │ ├── Contract.php │ │ │ ├── Mail.php │ │ │ ├── Slack.php │ │ │ └── BaseChannel.php │ │ └── Status.php │ ├── Facades │ │ └── Config.php │ ├── Events │ │ ├── UserNotifiedOfFailure.php │ │ ├── TestsFailed.php │ │ └── DataUpdated.php │ ├── Console │ │ └── Commands │ │ │ ├── BaseCommand.php │ │ │ ├── TestCommand.php │ │ │ └── WatchCommand.php │ ├── Data │ │ ├── Models │ │ │ ├── Queue.php │ │ │ ├── Run.php │ │ │ ├── Project.php │ │ │ ├── Tester.php │ │ │ ├── User.php │ │ │ ├── Model.php │ │ │ ├── Test.php │ │ │ └── Suite.php │ │ └── Repositories │ │ │ ├── Support │ │ │ ├── Notifications.php │ │ │ ├── Messages.php │ │ │ ├── Testers.php │ │ │ ├── Runs.php │ │ │ ├── Queue.php │ │ │ ├── Suites.php │ │ │ ├── Projects.php │ │ │ └── Tests.php │ │ │ └── Data.php │ ├── Http │ │ └── Controllers │ │ │ ├── Html.php │ │ │ ├── Dashboard.php │ │ │ ├── Files.php │ │ │ ├── Tests.php │ │ │ ├── Controller.php │ │ │ └── Projects.php │ ├── Listeners │ │ ├── MarkAsNotified.php │ │ └── Notify.php │ ├── Services │ │ ├── Cache.php │ │ ├── Base.php │ │ ├── Config.php │ │ └── Loader.php │ └── ServiceProvider.php └── public │ └── img │ └── ring-spinner.svg ├── .gitattributes ├── .babelrc ├── upgrading.md ├── docs ├── atoum.png ├── behat.png ├── video.png ├── phpspec.png ├── tester.png ├── dashboard.png ├── errorlog1.png ├── errorlog2.png ├── errorlog3.png └── acr-screenshot 2017-10-02 20.25.27.png ├── .gitignore ├── mix-manifest.json ├── fonts └── vendor │ ├── font-awesome │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ └── fontawesome-webfont.woff2 │ └── bootstrap-sass │ └── bootstrap │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── tests ├── DashboardTest.php ├── bootstrap.php └── TestCase.php ├── .scrutinizer.yml ├── .travis.yml ├── LICENSE.md ├── phpunit.xml ├── package.json ├── webpack.mix.js ├── composer.json ├── CHANGELOG.md └── README.md /src/routes/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/config/tddd/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/database/migrations/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/resources/assets/js/constants.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | src/public/* linguist-vendored 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["transform-object-rest-spread"] 3 | } 4 | -------------------------------------------------------------------------------- /upgrading.md: -------------------------------------------------------------------------------- 1 | # Laravel Stats SDK Upgrading Guide 2 | 3 | ## to 0.1.0 4 | 5 | -------------------------------------------------------------------------------- /docs/atoum.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/tddd/HEAD/docs/atoum.png -------------------------------------------------------------------------------- /docs/behat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/tddd/HEAD/docs/behat.png -------------------------------------------------------------------------------- /docs/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/tddd/HEAD/docs/video.png -------------------------------------------------------------------------------- /docs/phpspec.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/tddd/HEAD/docs/phpspec.png -------------------------------------------------------------------------------- /docs/tester.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/tddd/HEAD/docs/tester.png -------------------------------------------------------------------------------- /docs/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/tddd/HEAD/docs/dashboard.png -------------------------------------------------------------------------------- /docs/errorlog1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/tddd/HEAD/docs/errorlog1.png -------------------------------------------------------------------------------- /docs/errorlog2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/tddd/HEAD/docs/errorlog2.png -------------------------------------------------------------------------------- /docs/errorlog3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/tddd/HEAD/docs/errorlog3.png -------------------------------------------------------------------------------- /src/config/tddd/pipers/tee.yml: -------------------------------------------------------------------------------- 1 | bin: "/usr/bin/tee" 2 | execute: "{$command} | {$bin} > {$tempFile}" 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .DS_Store 5 | .idea 6 | /node_modules 7 | /coverage 8 | -------------------------------------------------------------------------------- /src/config/tddd/pipers/script-macos.yml: -------------------------------------------------------------------------------- 1 | bin: "/usr/bin/script" 2 | execute: "{$bin} -q {$tempFile} {$command}" 3 | -------------------------------------------------------------------------------- /src/package/Support/jasonlewis/resource-watcher/.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .DS_Store -------------------------------------------------------------------------------- /src/config/tddd/editors/sublime.yml: -------------------------------------------------------------------------------- 1 | code: sublime 2 | name: SublimeText 3 3 | bin: "/usr/local/bin/subl {file}:{line}" 4 | -------------------------------------------------------------------------------- /src/config/tddd/pipers/script-debian.yml: -------------------------------------------------------------------------------- 1 | bin: "/usr/bin/script" 2 | execute: "{$bin} -q -c '{$command}' {$tempFile}" 3 | -------------------------------------------------------------------------------- /src/config/tddd/testers/atoum.yml: -------------------------------------------------------------------------------- 1 | code: atoum 2 | name: Atoum 3 | command: sh vendor/bin/atoum 4 | pipers: 5 | - tee 6 | -------------------------------------------------------------------------------- /docs/acr-screenshot 2017-10-02 20.25.27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/tddd/HEAD/docs/acr-screenshot 2017-10-02 20.25.27.png -------------------------------------------------------------------------------- /mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/src/public/js/app.js": "/src/public/js/app.js", 3 | "/src/public/css/app.css": "/src/public/css/app.css" 4 | } -------------------------------------------------------------------------------- /src/config/tddd/editors/phpstorm.yml: -------------------------------------------------------------------------------- 1 | code: phpstorm 2 | name: PHPStorm 3 | bin: "/usr/local/bin/pstorm {file}:{line}" 4 | default: true 5 | -------------------------------------------------------------------------------- /fonts/vendor/font-awesome/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/tddd/HEAD/fonts/vendor/font-awesome/fontawesome-webfont.eot -------------------------------------------------------------------------------- /fonts/vendor/font-awesome/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/tddd/HEAD/fonts/vendor/font-awesome/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /fonts/vendor/font-awesome/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/tddd/HEAD/fonts/vendor/font-awesome/fontawesome-webfont.woff -------------------------------------------------------------------------------- /fonts/vendor/font-awesome/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/tddd/HEAD/fonts/vendor/font-awesome/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /src/config/tddd/testers/behat.yml: -------------------------------------------------------------------------------- 1 | code: behat 2 | name: Behat 3 | command: sh vendor/bin/behat 4 | pipers: 5 | - "script-{{ config('tddd-base.host_os') }}" 6 | -------------------------------------------------------------------------------- /src/config/tddd/testers/phpspec.yml: -------------------------------------------------------------------------------- 1 | code: phpspec 2 | name: phpspec 3 | command: phpspec run 4 | pipers: 5 | - "script-{{ config('tddd-base.host_os') }}" 6 | -------------------------------------------------------------------------------- /src/config/tddd/testers/phpunit.yml: -------------------------------------------------------------------------------- 1 | code: phpunit 2 | name: PHPUnit 3 | command: vendor/bin/phpunit 4 | pipers: 5 | - "script-{{ config('tddd-base.host_os') }}" 6 | -------------------------------------------------------------------------------- /src/config/tddd/testers/tester.yml: -------------------------------------------------------------------------------- 1 | code: tester 2 | name: Tester 3 | command: sh vendor/bin/tester 4 | pipers: 5 | - "script-{{ config('tddd-base.host_os') }}" 6 | -------------------------------------------------------------------------------- /src/config/tddd/editors/vscode.yml: -------------------------------------------------------------------------------- 1 | code: vscode 2 | name: VSCode 3 | bin: "/Applications/Visual\\ Studio\\ Code.app/Contents/Resources/app/bin/code --goto {file}:{line}" 4 | -------------------------------------------------------------------------------- /src/config/tddd/routes.yml: -------------------------------------------------------------------------------- 1 | prefixes: 2 | global: '' 3 | dashboard: '' 4 | tests: "/tests" 5 | projects: "/projects" 6 | files: "/files" 7 | html: "/html" 8 | -------------------------------------------------------------------------------- /fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/tddd/HEAD/fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/tddd/HEAD/fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/tddd/HEAD/fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/tddd/HEAD/fonts/vendor/bootstrap-sass/bootstrap/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /src/config/tddd/testers/rake.yml: -------------------------------------------------------------------------------- 1 | code: rake 2 | name: Rake 3 | command: bin/rails test 4 | error_pattern: Test\s+Suites:\s+[0-9]+\s+failed 5 | pipers: 6 | - "script-{{ config('tddd-base.host_os') }}" 7 | -------------------------------------------------------------------------------- /src/config/tddd/testers/simple-phpunit.yml: -------------------------------------------------------------------------------- 1 | code: simple-phpunit 2 | name: Simple PHPUnit (Symfony) 3 | command: vendor/bin/simple-phpunit 4 | pipers: 5 | - "script-{{ config('tddd-base.host_os') }}" 6 | -------------------------------------------------------------------------------- /src/config/tddd/testers/ava.yml: -------------------------------------------------------------------------------- 1 | code: ava 2 | name: AVA 3 | command: node_modules/.bin/ava --verbose 4 | error_pattern: "[1-9]+\\s+(exception|failure)" 5 | pipers: 6 | - "script-{{ config('tddd-base.host_os') }}" 7 | -------------------------------------------------------------------------------- /src/config/tddd/testers/jest.yml: -------------------------------------------------------------------------------- 1 | code: jest 2 | name: Jest 3 | command: npm test 4 | output_folder: tests/__snapshots__ 5 | output_html_fail_extension: ".snap" 6 | pipers: 7 | - "script-{{ config('tddd-base.host_os') }}" 8 | -------------------------------------------------------------------------------- /src/config/tddd/testers/react-scripts.yml: -------------------------------------------------------------------------------- 1 | code: react-scripts 2 | name: React Scripts (Tester) 3 | env: CI=true 4 | command: npm test 5 | error_pattern: Test\s+Suites:\s+[0-9]+\s+failed 6 | pipers: 7 | - "script-{{ config('tddd-base.host_os') }}" 8 | -------------------------------------------------------------------------------- /src/config/tddd/testers/codeception.yml: -------------------------------------------------------------------------------- 1 | code: codeception 2 | name: Codeception 3 | command: sh {$project_path}/vendor/bin/codecept run 4 | output_folder: tests/_output 5 | output_html_fail_extension: ".fail.html" 6 | output_png_fail_extension: ".fail.png" 7 | pipers: 8 | - "script-{{ config('tddd-base.host_os') }}" 9 | -------------------------------------------------------------------------------- /src/package/Notifications/Channels/Contract.php: -------------------------------------------------------------------------------- 1 | env('TDDD_CONFIG_PATH', __DIR__), 9 | 10 | 'host_os' => env('TDDD_HOST_OS', __DIR__), 11 | 12 | 'user_home' => env('HOME', __DIR__), 13 | ]; 14 | -------------------------------------------------------------------------------- /src/package/Support/jasonlewis/resource-watcher/src/JasonLewis/ResourceWatcher/Resource/ResourceInterface.php: -------------------------------------------------------------------------------- 1 | tests = $tests; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/package/Console/Commands/BaseCommand.php: -------------------------------------------------------------------------------- 1 | line(str_repeat('-', max($len, 80))); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/package/Data/Models/Queue.php: -------------------------------------------------------------------------------- 1 | belongsTo('PragmaRX\Tddd\Package\Data\Models\Test'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/resources/assets/js/bootstrap-app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * lodash _ 3 | */ 4 | 5 | window._ = require('lodash'); 6 | 7 | /** 8 | * jQuery & Bootstrap 9 | */ 10 | 11 | window.$ = window.jQuery = require('jquery'); 12 | 13 | window.Popper = require('popper.js'); 14 | 15 | window.bootstrap = require('bootstrap'); 16 | 17 | const Pusher = require('pusher-js'); 18 | 19 | /** 20 | * Axios 21 | */ 22 | 23 | window.axios = require('axios'); 24 | 25 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 26 | -------------------------------------------------------------------------------- /src/package/Http/Controllers/Html.php: -------------------------------------------------------------------------------- 1 | get('index')), 14 | public_path(config('tddd.root.coverage.path')) 15 | ); 16 | 17 | return redirect()->to('/'.config('tddd.root.coverage.path').'/'.basename($index)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/package/Events/TestsFailed.php: -------------------------------------------------------------------------------- 1 | tests = $tests; 25 | 26 | $this->channel = $channel; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/package/Data/Models/Run.php: -------------------------------------------------------------------------------- 1 | path, 25 | $this->tests_path, 26 | ] 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/resources/lang/en/pagination.php: -------------------------------------------------------------------------------- 1 | '« Previous', 17 | 'next' => 'Next »', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /src/package/Listeners/MarkAsNotified.php: -------------------------------------------------------------------------------- 1 | dataRepository = $dataRepository; 13 | } 14 | 15 | /** 16 | * Handle the event. 17 | * 18 | * @param UserNotifiedOfFailure $event 19 | * 20 | * @return void 21 | */ 22 | public function handle(UserNotifiedOfFailure $event) 23 | { 24 | $this->dataRepository->markTestsAsNotified($event->tests); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/DashboardTest.php: -------------------------------------------------------------------------------- 1 | watcher = app(Watcher::class); 18 | 19 | $this->worker = app(Tester::class); 20 | } 21 | 22 | public function test_can_instantiate_watcher() 23 | { 24 | $this->assertInstanceOf(Watcher::class, $this->watcher); 25 | 26 | $this->assertInstanceOf(Tester::class, $this->worker); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/resources/lang/en/auth.php: -------------------------------------------------------------------------------- 1 | 'These credentials do not match our records.', 17 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', 18 | 19 | ]; 20 | -------------------------------------------------------------------------------- /src/config/tddd/projects/phpunit-example.yml: -------------------------------------------------------------------------------- 1 | ## More examples at: 2 | 3 | name: "PHPUnit Example" 4 | path: "{{ config('tddd.root.code.path') }}/pragmarx/tddd" # absolute 5 | watch_folders: 6 | - app # all other directories are relative 7 | - tests # watching tests folder so it runs only the changed test 8 | exclude: [] 9 | depends: [] 10 | tests_path: tests # root tests path relative to $path 11 | suites: 12 | unit: 13 | tester: phpunit 14 | tests_path: Unit # relative to $tests_path 15 | command_options: '' 16 | file_mask: "*Test.php" 17 | retries: 0 18 | functional: 19 | tester: dusk 20 | tests_path: Browser 21 | command_options: '' 22 | file_mask: "*Test.php" 23 | retries: 0 24 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | checks: 2 | php: 3 | remove_extra_empty_lines: true 4 | remove_php_closing_tag: true 5 | remove_trailing_whitespace: true 6 | fix_use_statements: 7 | remove_unused: true 8 | preserve_multiple: false 9 | preserve_blanklines: true 10 | order_alphabetically: true 11 | fix_php_opening_tag: true 12 | fix_linefeed: true 13 | fix_line_ending: true 14 | fix_identation_4spaces: true 15 | fix_doc_comments: true 16 | 17 | filter: 18 | paths: [src/*] 19 | excluded_paths: ["tests/*", "update/*"] 20 | 21 | coding_style: 22 | php: { } 23 | 24 | tools: 25 | external_code_coverage: true 26 | -------------------------------------------------------------------------------- /src/package/Support/jasonlewis/resource-watcher/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jasonlewis/resource-watcher", 3 | "description": "Simple PHP resource watcher library.", 4 | "keywords": ["resource", "assets", "laravel"], 5 | "authors": [ 6 | { 7 | "name": "Jason Lewis", 8 | "email": "jason.lewis1991@gmail.com" 9 | } 10 | ], 11 | "require": { 12 | "php": ">=5.3.0", 13 | "illuminate/support": "~4.0", 14 | "illuminate/filesystem": "~4.0" 15 | }, 16 | "require-dev": { 17 | "mockery/mockery": "~0.9" 18 | }, 19 | "autoload": { 20 | "psr-0": { 21 | "JasonLewis\\ResourceWatcher": "src/" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/package/Notifications/Channels/Mail.php: -------------------------------------------------------------------------------- 1 | line($this->getMessage($item)) 19 | ->from( 20 | config('tddd.notifications.from.address'), 21 | config('tddd.notifications.from.name') 22 | ) 23 | ->action($this->getActionTitle(), $this->getActionLink()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/public/img/ring-spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/package/Http/Controllers/Dashboard.php: -------------------------------------------------------------------------------- 1 | with('laravel', $this->dataRepository->getJavascriptClientData()); 17 | } 18 | 19 | /** 20 | * Dashboard index. 21 | * 22 | * @return \Illuminate\Http\Response 23 | */ 24 | public function data() 25 | { 26 | return $this->success([ 27 | 'projects' => $this->dataRepository->getProjectsAndCacheResult(), 28 | ]); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/config/tddd/notifications.yml: -------------------------------------------------------------------------------- 1 | notify_on: 2 | fail: true 3 | pass: false 4 | routes: 5 | dashboard: tests-watcher.dashboard 6 | action-title: Tests Failed 7 | action_message: One or more tests have failed. 8 | from: 9 | name: Test Driven Development Dashboard 10 | address: tddd@mydomain.com 11 | icon_emoji: '' 12 | icon_url: https://emojipedia-us.s3.amazonaws.com/thumbs/120/apple/96/lady-beetle_1f41e.png 13 | users: 14 | model: PragmaRX\Tddd\Package\Data\Models\User 15 | emails: 16 | - tddd@mydomain.com 17 | channels: 18 | mail: 19 | enabled: false 20 | sender: PragmaRX\Tddd\Package\Notifications\Channels\Mail 21 | slack: 22 | enabled: true 23 | sender: PragmaRX\Tddd\Package\Notifications\Channels\Slack 24 | notifier: PragmaRX\Tddd\Notifications 25 | -------------------------------------------------------------------------------- /src/database/migrations/2017_10_09_800008_add_sha1.php: -------------------------------------------------------------------------------- 1 | string('sha1')->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function migrateDown() 27 | { 28 | Schema::table('tddd_tests', function (Blueprint $table) { 29 | $table->dropColumn('sha1'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/database/migrations/2017_10_10_800009_add_env.php: -------------------------------------------------------------------------------- 1 | string('env')->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function migrateDown() 27 | { 28 | Schema::table('tddd_testers', function (Blueprint $table) { 29 | $table->dropColumn('env'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | language: php 3 | 4 | php: 5 | - 7.0 6 | - 7.1 7 | #- 7.2 8 | 9 | # This triggers builds to run on the new TravisCI infrastructure. 10 | # See: http://docs.travis-ci.com/user/workers/container-based-infrastructure/ 11 | sudo: false 12 | 13 | ## Cache composer 14 | cache: 15 | directories: 16 | - $HOME/.composer/cache 17 | 18 | before_script: 19 | - travis_retry composer update ${COMPOSER_FLAGS} --no-interaction --prefer-dist 20 | 21 | script: 22 | - vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover 23 | 24 | after_script: 25 | - | 26 | if [[ "$TRAVIS_PHP_VERSION" != 'hhvm' && "$TRAVIS_PHP_VERSION" != '7.0' ]]; then 27 | wget https://scrutinizer-ci.com/ocular.phar 28 | php ocular.phar code-coverage:upload --format=php-clover coverage.clover 29 | fi 30 | -------------------------------------------------------------------------------- /src/database/migrations/2017_10_10_800010_add_editor.php: -------------------------------------------------------------------------------- 1 | string('editor')->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function migrateDown() 27 | { 28 | Schema::table('tddd_suites', function (Blueprint $table) { 29 | $table->dropColumn('editor'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/database/migrations/2017_10_11_800011_add_sha1_index.php: -------------------------------------------------------------------------------- 1 | index('sha1'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function migrateDown() 27 | { 28 | Schema::table('tddd_tests', function (Blueprint $table) { 29 | $table->dropIndex('tddd_tests_sha1_index'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/package/Console/Commands/TestCommand.php: -------------------------------------------------------------------------------- 1 | getLaravel()->make('tddd.tester')->run($this); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/database/migrations/2017_09_30_800003_add_uses_tee.php: -------------------------------------------------------------------------------- 1 | boolean('require_tee')->default(false); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function migrateDown() 27 | { 28 | Schema::table('tddd_testers', function (Blueprint $table) { 29 | $table->dropColumn('require_tee'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/database/migrations/2017_09_25_800001_create_runs_indexes.php: -------------------------------------------------------------------------------- 1 | index('created_at'); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function migrateDown() 27 | { 28 | Schema::table('tddd_runs', function (Blueprint $table) { 29 | $table->dropIndex('tddd_runs_created_at_index'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/database/migrations/2017_10_11_800012_add_project_enabled.php: -------------------------------------------------------------------------------- 1 | boolean('enabled')->default(true); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function migrateDown() 27 | { 28 | Schema::table('tddd_projects', function (Blueprint $table) { 29 | $table->dropColumn('enabled'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/resources/assets/js/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Application bootstrap 3 | */ 4 | 5 | require('./bootstrap-app'); 6 | 7 | /** 8 | * Vue 9 | */ 10 | 11 | window.Vue = require('vue'); 12 | window.Vuex = require('vuex'); 13 | 14 | /** 15 | * Vuex store 16 | */ 17 | 18 | Vue.use(Vuex); 19 | 20 | import store from './store'; 21 | 22 | /** 23 | * Axios 24 | */ 25 | 26 | Vue.prototype.$http = window.axios; 27 | 28 | /** 29 | * Load components 30 | */ 31 | 32 | Vue.component('projects', require('./components/Projects.vue')); 33 | Vue.component('tests', require('./components/Tests.vue')); 34 | Vue.component('state', require('./components/State.vue')); 35 | Vue.component('log', require('./components/Log.vue')); 36 | 37 | /** 38 | * Start application 39 | */ 40 | 41 | 42 | new Vue({ 43 | el: '#app', 44 | 45 | store: new Vuex.Store(store), 46 | }); 47 | -------------------------------------------------------------------------------- /src/database/migrations/2017_10_01_800005_add_regex_pattern.php: -------------------------------------------------------------------------------- 1 | string('error_pattern')->nullable(); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function migrateDown() 27 | { 28 | Schema::table('tddd_testers', function (Blueprint $table) { 29 | $table->dropColumn('error_pattern'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/database/migrations/2017_10_08_800007_add_require_script.php: -------------------------------------------------------------------------------- 1 | boolean('require_script')->default(false); 18 | }); 19 | } 20 | 21 | /** 22 | * Reverse the migrations. 23 | * 24 | * @return void 25 | */ 26 | public function migrateDown() 27 | { 28 | Schema::table('tddd_testers', function (Blueprint $table) { 29 | $table->dropColumn('require_script'); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/package/Data/Models/Tester.php: -------------------------------------------------------------------------------- 1 | mapWithKeys(function ($piper) { 25 | return [$piper => config("tddd.pipers.{$piper}")]; 26 | }); 27 | } 28 | 29 | public function setPipersAttribute($value) 30 | { 31 | $this->attributes['pipers'] = collect($value)->toJson(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/resources/lang/en/passwords.php: -------------------------------------------------------------------------------- 1 | 'Passwords must be at least six characters and match the confirmation.', 17 | 'reset' => 'Your password has been reset!', 18 | 'sent' => 'We have e-mailed your password reset link!', 19 | 'token' => 'This password reset token is invalid.', 20 | 'user' => "We can't find a user with that e-mail address.", 21 | 22 | ]; 23 | -------------------------------------------------------------------------------- /src/config/tddd/root.yml: -------------------------------------------------------------------------------- 1 | names: 2 | dashboard: {{ config('app.name') }} 3 | watcher: TDDD - Watcher 4 | worker: TDDD - Worker 5 | regex_file_matcher: "/([A-Za-z0-9\\/._-]+)(?::| on line )([1-9][0-9]*)/" 6 | poll: 7 | enabled: false 8 | interval: 300 # milliseconds 9 | tmp_dir: "/var/tmp/" 10 | show_progress: false 11 | cache: 12 | event_timeout: 10 # seconds 13 | instance: 'cache' 14 | code: 15 | path: "{{ config('tddd-base.user_home') }}/code" 16 | coverage: 17 | path: coverage # under /public 18 | broadcasting: 19 | enabled: true 20 | pusher: 21 | key: "{{ config('broadcasting.connections.pusher.key') }}" 22 | secret: "{{ config('broadcasting.connections.pusher.secret') }}" 23 | app_id: "{{ config('broadcasting.connections.pusher.app_id') }}" 24 | cluster: "{{ env('PUSHER_CLUSTER') }}" 25 | channel_name: "tddd" 26 | cache: 27 | enabled: true 28 | instance: 'cache' 29 | -------------------------------------------------------------------------------- /src/database/migrations/2017_10_01_800006_add_test_path.php: -------------------------------------------------------------------------------- 1 | string('path')->nullable(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function migrateDown() 30 | { 31 | Schema::table('tddd_tests', function (Blueprint $table) { 32 | $table->dropColumn('path'); 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/package/Console/Commands/WatchCommand.php: -------------------------------------------------------------------------------- 1 | getLaravel()->make('tddd.watcher')->run($this, $this->option('show-tests')); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/database/migrations/2017_09_30_800004_add_screenshots.php: -------------------------------------------------------------------------------- 1 | text('screenshots')->nullable(); 18 | $table->dropColumn('png'); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | * @return void 26 | */ 27 | public function migrateDown() 28 | { 29 | Schema::table('tddd_runs', function (Blueprint $table) { 30 | $table->dropColumn('screenshots'); 31 | $table->text('png')->nullable(); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/database/migrations/2014_11_14_300000_create_tddd_projects_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | 19 | $table->string('name'); 20 | 21 | $table->string('path'); 22 | 23 | $table->string('tests_path'); 24 | 25 | $table->timestamps(); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | * 32 | * @return void 33 | */ 34 | public function migrateDown() 35 | { 36 | Schema::drop('tddd_projects'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/database/migrations/2017_12_07_800015_add_coverage.php: -------------------------------------------------------------------------------- 1 | string('coverage_enabled')->boolean(false); 18 | 19 | $table->string('coverage_index')->nullable(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function migrateDown() 29 | { 30 | Schema::table('tddd_suites', function (Blueprint $table) { 31 | $table->dropColumn('coverage_enabled'); 32 | 33 | $table->dropColumn('coverage_index'); 34 | }); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/package/Http/Controllers/Files.php: -------------------------------------------------------------------------------- 1 | executor->exec( 19 | $command = $this->dataRepository->makeEditFileCommand($fileName, $line, $suite_id) 20 | ); 21 | 22 | return $this->success(); 23 | } 24 | 25 | /** 26 | * Download an image. 27 | * 28 | * @param $filename 29 | * 30 | * @return \Symfony\Component\HttpFoundation\BinaryFileResponse 31 | */ 32 | public function imageDownload($filename) 33 | { 34 | return response()->download( 35 | base64_decode($filename) 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/package/Data/Models/User.php: -------------------------------------------------------------------------------- 1 | routeNotificationForSlack(); 16 | } 17 | 18 | return $this->routeNotificationForEmail(); 19 | } 20 | 21 | /** 22 | * Route notifications for e-mail. 23 | * 24 | * @return string 25 | */ 26 | private function routeNotificationForEmail() 27 | { 28 | return config('tddd.notifications.user.email'); 29 | } 30 | 31 | /** 32 | * Route notifications for slack. 33 | * 34 | * @return \Illuminate\Config\Repository|mixed 35 | */ 36 | private function routeNotificationForSlack() 37 | { 38 | return config('services.slack.webhook_url'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/package/Listeners/Notify.php: -------------------------------------------------------------------------------- 1 | map(function ($item) { 17 | $model = instantiate(config('tddd.notifications.users.model')); 18 | 19 | $model->email = $item; 20 | 21 | return $model; 22 | }); 23 | } 24 | 25 | /** 26 | * Handle the event. 27 | * 28 | * @param TestsFailed $event 29 | * 30 | * @return void 31 | */ 32 | public function handle(TestsFailed $event) 33 | { 34 | Notification::send( 35 | $this->getNotifiableUsers(), 36 | new Status($event->tests, $event->channel) 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/database/migrations/2017_11_03_800014_add_pipers.php: -------------------------------------------------------------------------------- 1 | dropColumn('require_tee'); 18 | 19 | $table->dropColumn('require_script'); 20 | 21 | $table->text('pipers'); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | * 28 | * @return void 29 | */ 30 | public function migrateDown() 31 | { 32 | Schema::table('tddd_testers', function (Blueprint $table) { 33 | $table->boolean('require_tee'); 34 | 35 | $table->boolean('require_script'); 36 | 37 | $table->dropColumn('pipers'); 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/package/Data/Repositories/Support/Notifications.php: -------------------------------------------------------------------------------- 1 | notifier; 20 | } 21 | 22 | /** 23 | * Notify users. 24 | * 25 | * @param $project_id 26 | */ 27 | public function notify($project_id) 28 | { 29 | $this->notifier->notifyViaChannels( 30 | $this->getProjectTests($project_id)->reject(function ($item) { 31 | return $item['state'] != 'failed' && is_null($item['notified_at']); 32 | }) 33 | ); 34 | } 35 | 36 | /** 37 | * @param Notifier $notifier 38 | */ 39 | public function setNotifier($notifier) 40 | { 41 | $this->notifier = $notifier; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/resources/assets/js/components/State.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 39 | -------------------------------------------------------------------------------- /src/package/Data/Repositories/Support/Messages.php: -------------------------------------------------------------------------------- 1 | messages; 22 | } 23 | 24 | /** 25 | * Add a message to the list. 26 | * 27 | * @param $type 28 | * @param $body 29 | * 30 | * @internal param $string 31 | * @internal param $string1 32 | */ 33 | protected function addMessage($body, $type = 'line') 34 | { 35 | $this->messages->push(['type' => $type, 'body' => $body]); 36 | } 37 | 38 | /** 39 | * Set messages. 40 | * 41 | * @param \Illuminate\Support\Collection $messages 42 | */ 43 | public function setMessages($messages) 44 | { 45 | $this->messages = $messages; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/package/Support/jasonlewis/resource-watcher/src/JasonLewis/ResourceWatcher/Integration/LaravelServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton('watcher', function ($app) { 26 | $tracker = new Tracker(); 27 | 28 | return new Watcher($tracker, $app['files']); 29 | }); 30 | } 31 | 32 | /** 33 | * Get the services provided by the provider. 34 | * 35 | * @return array 36 | */ 37 | public function provides() 38 | { 39 | return ['watcher']; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/database/migrations/2014_11_14_200000_create_tddd_testers_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | 19 | $table->string('name'); 20 | 21 | $table->string('command'); 22 | 23 | $table->string('output_folder')->nullable(); 24 | 25 | $table->string('output_html_fail_extension')->nullable(); 26 | 27 | $table->string('output_png_fail_extension')->nullable(); 28 | 29 | $table->timestamps(); 30 | }); 31 | } 32 | 33 | /** 34 | * Reverse the migrations. 35 | * 36 | * @return void 37 | */ 38 | public function migrateDown() 39 | { 40 | Schema::drop('tddd_testers'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/package/Data/Repositories/Data.php: -------------------------------------------------------------------------------- 1 | setAnsiConverter(new AnsiToHtmlConverter()); 27 | 28 | $this->setNotifier($notifier); 29 | 30 | $this->setMessages(collect()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | 19 | $table->integer('test_id')->unsigned(); 20 | 21 | $table->timestamps(); 22 | }); 23 | 24 | Schema::table('tddd_queue', function (Blueprint $table) { 25 | $table->foreign('test_id') 26 | ->references('id') 27 | ->on('tddd_tests') 28 | ->onDelete('cascade') 29 | ->onUpdate('cascade'); 30 | }); 31 | } 32 | 33 | /** 34 | * Reverse the migrations. 35 | * 36 | * @return void 37 | */ 38 | public function migrateDown() 39 | { 40 | Schema::drop('tddd_queue'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/package/Http/Controllers/Tests.php: -------------------------------------------------------------------------------- 1 | dataRepository->reset($project_id); 17 | 18 | return $this->success(); 19 | } 20 | 21 | /** 22 | * Run a test. 23 | * 24 | * @param $test_id 25 | * 26 | * @return mixed 27 | */ 28 | public function run($test_id) 29 | { 30 | $this->dataRepository->runTest($test_id); 31 | 32 | return $this->success(); 33 | } 34 | 35 | /** 36 | * Enable tests. 37 | * 38 | * @param $enable 39 | * @param $project_id 40 | * @param null $test_id 41 | * 42 | * @return mixed 43 | */ 44 | public function enable($project_id, $test_id, $enable) 45 | { 46 | $enabled = $this->dataRepository->enableTests($enable, $project_id, $test_id); 47 | 48 | return $this->success(['enabled' => $enabled]); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/database/migrations/2017_09_27_800002_create_runs_start_end.php: -------------------------------------------------------------------------------- 1 | timestamp('started_at')->nullable(); 21 | 22 | $table->timestamp('ended_at')->nullable(); 23 | 24 | $table->timestamp('notified_at')->nullable(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function migrateDown() 34 | { 35 | Schema::table('tddd_runs', function (Blueprint $table) { 36 | $table->dropColumn('started_at'); 37 | 38 | $table->dropColumn('ended_at'); 39 | 40 | $table->dropColumn('notified_at'); 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/package/Notifications/Channels/Slack.php: -------------------------------------------------------------------------------- 1 | error() 19 | ->from( 20 | config('tddd.notifications.from.name'), 21 | $icon = (config('tddd.notifications.from.icon_emoji') ?: null) 22 | ) 23 | ->content($this->getMessage($tests)); 24 | 25 | if (is_null($icon)) { 26 | $notification->image(config('tddd.notifications.from.icon_url') ?: null); 27 | } 28 | 29 | $tests->each(function ($test) use ($notification) { 30 | $notification->attachment(function ($attachment) use ($test) { 31 | $attachment->title($this->makeActionTitle($test), $this->makeActionLink($test)); 32 | }); 33 | }); 34 | 35 | return $notification; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2013-2017 Antonio Carlos Ribeiro acr@antoniocarlosribeiro.com 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 13 | > all 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 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/package/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | dataRepository = $dataRepository; 31 | 32 | $this->executor = $executor; 33 | } 34 | 35 | /** 36 | * Return a success response. 37 | * 38 | * @param array $result 39 | * 40 | * @return \Illuminate\Http\Response 41 | */ 42 | public function success($result = []) 43 | { 44 | return Response::json(array_merge(['success' => true], $result)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/package/Services/Cache.php: -------------------------------------------------------------------------------- 1 | getCacheInstance()->put($key, $value, $minutes); 24 | 25 | return $value; 26 | } 27 | 28 | /** 29 | * Get a value from the cache store. 30 | * 31 | * @throws \Exception 32 | * 33 | * @return mixed 34 | */ 35 | public function get($key) 36 | { 37 | $cached = $this->getCacheInstance()->get($key); 38 | 39 | return $cached; 40 | } 41 | 42 | /** 43 | * Get the cache instance. 44 | * 45 | * @throws \Exception 46 | * 47 | * @return array|\Illuminate\Foundation\Application|mixed 48 | */ 49 | protected function getCacheInstance() 50 | { 51 | if (!$this->cache) { 52 | $this->cache = app($this->config('root.cache.instance')); 53 | } 54 | 55 | return $this->cache; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/package/Support/jasonlewis/resource-watcher/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Jason Lewis 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /src/database/migrations/2014_11_14_800000_create_tddd_runs_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | 19 | $table->integer('test_id')->unsigned(); 20 | 21 | $table->boolean('was_ok'); 22 | 23 | $table->mediumText('log'); 24 | 25 | $table->text('html')->nullable(); 26 | 27 | $table->text('png')->nullable(); 28 | 29 | $table->timestamps(); 30 | }); 31 | 32 | Schema::table('tddd_runs', function (Blueprint $table) { 33 | $table->foreign('test_id') 34 | ->references('id') 35 | ->on('tddd_tests') 36 | ->onDelete('cascade') 37 | ->onUpdate('cascade'); 38 | }); 39 | } 40 | 41 | /** 42 | * Reverse the migrations. 43 | * 44 | * @return void 45 | */ 46 | public function migrateDown() 47 | { 48 | Schema::drop('tddd_runs'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | ./tests 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/database/migrations/2014_11_14_500000_create_tddd_tests_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | 19 | $table->integer('suite_id')->unsigned(); 20 | 21 | $table->string('name'); 22 | 23 | $table->string('state')->default('idle'); 24 | 25 | $table->boolean('enabled')->default(true); 26 | 27 | $table->integer('last_run_id')->unsigned()->nullable(); 28 | 29 | $table->timestamps(); 30 | }); 31 | 32 | Schema::table('tddd_tests', function (Blueprint $table) { 33 | $table->foreign('suite_id') 34 | ->references('id') 35 | ->on('tddd_suites') 36 | ->onDelete('cascade') 37 | ->onUpdate('cascade'); 38 | }); 39 | } 40 | 41 | /** 42 | * Reverse the migrations. 43 | * 44 | * @return void 45 | */ 46 | public function migrateDown() 47 | { 48 | Schema::drop('tddd_tests'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/package/Data/Models/Model.php: -------------------------------------------------------------------------------- 1 | projectSha1HasChanged()) { 14 | broadcast(new DataUpdated()); 15 | } 16 | } 17 | 18 | /** 19 | * Save the model to the database. 20 | * 21 | * @param array $options 22 | * 23 | * @return bool 24 | */ 25 | public function save(array $options = []) 26 | { 27 | parent::save($options); 28 | 29 | $this->broadcastDataUpdated(); 30 | } 31 | 32 | /** 33 | * Get the connection of the entity. 34 | * 35 | * @return string|null 36 | */ 37 | public function getQueueableConnection() 38 | { 39 | // TODO: Implement getQueueableConnection() method. 40 | } 41 | 42 | /** 43 | * Retrieve the model for a bound value. 44 | * 45 | * @param mixed $value 46 | * 47 | * @return \Illuminate\Database\Eloquent\Model|null 48 | */ 49 | public function resolveRouteBinding($value) 50 | { 51 | // TODO: Implement resolveRouteBinding() method. 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/database/migrations/2017_10_27_800013_log_medium_text.php: -------------------------------------------------------------------------------- 1 | mediumText('log_new')->nullable()->after('was_ok'); 19 | }); 20 | 21 | Database::statement('update tddd_runs set log_new = log;'); 22 | 23 | Schema::table('tddd_runs', function (Blueprint $table) { 24 | $table->dropColumn('log'); 25 | $table->renameColumn('log_new', 'log'); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | * 32 | * @return void 33 | */ 34 | public function migrateDown() 35 | { 36 | Schema::table('tddd_runs', function (Blueprint $table) { 37 | $table->text('log_old')->nullable(); 38 | }); 39 | 40 | Database::statement('update tddd_runs set log_old = log;'); 41 | 42 | Schema::table('tddd_runs', function (Blueprint $table) { 43 | $table->dropColumn('log'); 44 | $table->renameColumn('log_old', 'log'); 45 | }); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/package/Support/helpers.php: -------------------------------------------------------------------------------- 1 | project->path, $string); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "npm run development", 5 | "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 6 | "watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 7 | "watch-poll": "npm run watch -- --watch-poll", 8 | "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js", 9 | "prod": "npm run production", 10 | "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" 11 | }, 12 | "devDependencies": { 13 | "axios": "^0.16.2", 14 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 15 | "bootstrap": "^4.0.0-beta", 16 | "cross-env": "^5.0.1", 17 | "csspin": "^1.1.4", 18 | "jquery": "^3.1.1", 19 | "laravel-mix": "^1.0", 20 | "less": "^2.7.2", 21 | "lodash": "^4.17.4", 22 | "node-sass": "^4.5.3", 23 | "popper.js": "^1.12.5", 24 | "pusher-js": "^4.3.0", 25 | "vue": "^2.1.10", 26 | "vuex": "^2.4.0", 27 | "webpack-livereload-plugin": "^0.11.0" 28 | }, 29 | "dependencies": {} 30 | } 31 | -------------------------------------------------------------------------------- /src/package/Support/Executor.php: -------------------------------------------------------------------------------- 1 | setTimeout($timeout); 32 | 33 | $this->startedAt = Carbon::now(); 34 | 35 | $process->run($callback); 36 | 37 | $this->endedAt = Carbon::now(); 38 | 39 | return $process; 40 | } 41 | 42 | /** 43 | * Get the elapsed time formatted for humans. 44 | * 45 | * @return mixed 46 | */ 47 | public function elapsedForHumans() 48 | { 49 | return $this->endedAt->diffForHumans($this->startedAt); 50 | } 51 | 52 | /** 53 | * Execute a shell command. 54 | * 55 | * @param $command 56 | * 57 | * @return mixed 58 | */ 59 | public function shellExec($command) 60 | { 61 | return shell_exec($command); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/database/migrations/2014_11_14_400000_create_tddd_suites_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | 19 | $table->string('name'); 20 | 21 | $table->integer('project_id')->unsigned(); 22 | 23 | $table->integer('tester_id')->unsigned(); 24 | 25 | $table->string('tests_path'); 26 | 27 | $table->string('file_mask'); 28 | 29 | $table->string('command_options'); 30 | 31 | $table->integer('retries')->default(0); 32 | 33 | $table->timestamps(); 34 | }); 35 | 36 | Schema::table('tddd_suites', function (Blueprint $table) { 37 | $table->foreign('project_id') 38 | ->references('id') 39 | ->on('tddd_projects') 40 | ->onDelete('cascade') 41 | ->onUpdate('cascade'); 42 | }); 43 | } 44 | 45 | /** 46 | * Reverse the migrations. 47 | * 48 | * @return void 49 | */ 50 | public function migrateDown() 51 | { 52 | Schema::drop('tddd_suites'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/package/Data/Repositories/Support/Testers.php: -------------------------------------------------------------------------------- 1 | $data['code']], 19 | [ 20 | 'command' => $data['command'], 21 | 'output_folder' => array_get($data, 'output_folder'), 22 | 'output_html_fail_extension' => array_get($data, 'output_html_fail_extension'), 23 | 'output_png_fail_extension' => array_get($data, 'output_png_fail_extension'), 24 | 'pipers' => array_get($data, 'pipers'), 25 | 'error_pattern' => array_get($data, 'error_pattern'), 26 | 'env' => array_get($data, 'env'), 27 | ] 28 | ); 29 | } 30 | 31 | /** 32 | * Delete unavailable testers. 33 | * 34 | * @param $testers 35 | */ 36 | public function deleteMissingTesters($testers) 37 | { 38 | foreach (Tester::all() as $tester) { 39 | if (!in_array($tester->name, $testers)) { 40 | $tester->delete(); 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/package/Notifications/Channels/BaseChannel.php: -------------------------------------------------------------------------------- 1 | getActionMessage($item); 34 | } 35 | 36 | /** 37 | * @return string 38 | */ 39 | protected function getActionLink() 40 | { 41 | return route(config('tddd.notifications.routes.dashboard')); 42 | } 43 | 44 | protected function makeActionTitle($test) 45 | { 46 | return "{$test['project_name']} - {$test['name']}"; 47 | } 48 | 49 | protected function makeActionLink($test) 50 | { 51 | return route( 52 | 'tests-watcher.dashboard', 53 | [ 54 | 'test_id' => $test['id'], 55 | 'project_id' => $test['project_id'], 56 | ] 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/package/Notifications/Status.php: -------------------------------------------------------------------------------- 1 | item = $item; 30 | 31 | $this->channel = $channel; 32 | } 33 | 34 | /** 35 | * @param $name 36 | * 37 | * @return \Illuminate\Foundation\Application|mixed 38 | */ 39 | private function getSenderInstance($name) 40 | { 41 | $name = substr($name, 2); 42 | 43 | return instantiate(config('tddd.notifications.channels.'.strtolower($name).'.sender')); 44 | } 45 | 46 | /** 47 | * Get the notification's delivery channels. 48 | * 49 | * @return array 50 | */ 51 | public function via() 52 | { 53 | return [$this->channel]; 54 | } 55 | 56 | /** 57 | * @param $name 58 | * @param $parameters 59 | * 60 | * @return mixed 61 | */ 62 | public function __call($name, $parameters) 63 | { 64 | $parameters[] = $this->item; 65 | 66 | return call_user_func_array( 67 | [ 68 | $this->getSenderInstance($name), 69 | 'send', 70 | ], 71 | $parameters 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/package/Http/Controllers/Projects.php: -------------------------------------------------------------------------------- 1 | dataRepository->enableProjects($enable, $project_id); 20 | 21 | return $this->success(['enabled' => $enabled]); 22 | } 23 | 24 | /** 25 | * Notify users. 26 | * 27 | * @param $project_id 28 | * 29 | * @return mixed 30 | */ 31 | public function notify($project_id) 32 | { 33 | $this->dataRepository->notify($project_id); 34 | 35 | return $this->success(); 36 | } 37 | 38 | /** 39 | * Run project tests. 40 | * 41 | * @return mixed 42 | */ 43 | public function run(Request $request) 44 | { 45 | $this->dataRepository->runProjectTests($request->get('projects')); 46 | 47 | return $this->success(); 48 | } 49 | 50 | /** 51 | * Reset projects tests states. 52 | * 53 | * @param Request $request 54 | * 55 | * @return mixed 56 | */ 57 | public function reset(Request $request) 58 | { 59 | $this->dataRepository->reset($request->get('projects')); 60 | 61 | return $this->success(); 62 | } 63 | 64 | /** 65 | * Toggle the enabled state of all projects. 66 | * 67 | * @return mixed 68 | */ 69 | public function toggleAll() 70 | { 71 | $this->dataRepository->toggleAll(); 72 | 73 | return $this->success(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | let mix = require('laravel-mix'); 2 | 3 | // var webpack = require('webpack'); 4 | 5 | // var LiveReloadPlugin = require('webpack-livereload-plugin'); 6 | 7 | mix.js('src/resources/assets/js/app.js', 'src/public/js') 8 | .sass('src/resources/assets/sass/app.scss', 'src/public/css'); 9 | 10 | // mix.webpackConfig({ 11 | // plugins: [ 12 | // // new LiveReloadPlugin(), 13 | // 14 | // // new webpack.ProvidePlugin({ 15 | // // $: "jquery", 16 | // // jQuery: "jquery", 17 | // // "window.jQuery": "jquery", 18 | // // Tether: "tether", 19 | // // "window.Tether": "tether", 20 | // // Alert: "exports-loader?Alert!bootstrap/js/dist/alert", 21 | // // Button: "exports-loader?Button!bootstrap/js/dist/button", 22 | // // Carousel: "exports-loader?Carousel!bootstrap/js/dist/carousel", 23 | // // Collapse: "exports-loader?Collapse!bootstrap/js/dist/collapse", 24 | // // Dropdown: "exports-loader?Dropdown!bootstrap/js/dist/dropdown", 25 | // // Modal: "exports-loader?Modal!bootstrap/js/dist/modal", 26 | // // Popover: "exports-loader?Popover!bootstrap/js/dist/popover", 27 | // // Scrollspy: "exports-loader?Scrollspy!bootstrap/js/dist/scrollspy", 28 | // // Tab: "exports-loader?Tab!bootstrap/js/dist/tab", 29 | // // Tooltip: "exports-loader?Tooltip!bootstrap/js/dist/tooltip", 30 | // // Util: "exports-loader?Util!bootstrap/js/dist/util", 31 | // // }) 32 | // ] 33 | // }); 34 | 35 | mix.webpackConfig({ 36 | node: { 37 | console: false, 38 | fs: 'empty', 39 | net: 'empty', 40 | tls: 'empty' 41 | } 42 | }); 43 | 44 | 45 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | app['config']->set("ci.{$key}", $value); 16 | } 17 | 18 | return $this->app['config']->get("ci.{$key}"); 19 | } 20 | 21 | private function configureDatabase() 22 | { 23 | if (!file_exists($path = __DIR__.'/databases')) { 24 | mkdir($path); 25 | } 26 | 27 | touch($this->database = tempnam($path, 'database.sqlite.')); 28 | 29 | app()->config->set( 30 | 'database.connections.testbench', 31 | [ 32 | 'driver' => 'sqlite', 33 | 'database' => $this->database, 34 | 'prefix' => '', 35 | ] 36 | ); 37 | } 38 | 39 | private function deleteDatabase() 40 | { 41 | @unlink($this->database); 42 | } 43 | 44 | protected function setUp() 45 | { 46 | dd('configure'); 47 | parent::setUp(); 48 | 49 | $this->configureDatabase(); 50 | 51 | $this->artisan('migrate:fresh', ['--database' => 'testbench']); 52 | } 53 | 54 | protected function tearDown() 55 | { 56 | parent::tearDown(); 57 | 58 | $this->deleteDatabase(); 59 | } 60 | 61 | protected function getPackageProviders($app) 62 | { 63 | $this->app = $app; 64 | 65 | return [ 66 | TdddServiceProvider::class, 67 | ]; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/package/Support/Notifier.php: -------------------------------------------------------------------------------- 1 | enabled()) { 32 | return false; 33 | } 34 | 35 | collect(config('tddd.notifications.channels'))->each(function ($value, $channel) use ($tests) { 36 | event(new TestsFailed($tests, $channel)); 37 | 38 | event(new UserNotifiedOfFailure($tests)); 39 | }); 40 | } 41 | 42 | /** 43 | * Send a notification. 44 | * 45 | * @param $title 46 | * @param $body 47 | * @param null $icon 48 | * 49 | * @return bool 50 | */ 51 | public function notifyViaDesktop($title, $body, $icon = null) 52 | { 53 | if (!static::enabled()) { 54 | return false; 55 | } 56 | 57 | $notifier = NotifierFactory::create(); 58 | 59 | $notification = 60 | (new Notification()) 61 | ->setTitle($title) 62 | ->setBody($body); 63 | 64 | if (!is_null($icon)) { 65 | $notification->setIcon('http://vjeantet.fr/images/logo.png'); 66 | } 67 | 68 | $notifier->send($notification); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/package/Support/jasonlewis/resource-watcher/src/JasonLewis/ResourceWatcher/Event.php: -------------------------------------------------------------------------------- 1 | resource = $resource; 55 | $this->code = $code; 56 | } 57 | 58 | /** 59 | * Get the resource event code. 60 | * 61 | * @return int 62 | */ 63 | public function getCode() 64 | { 65 | return $this->code; 66 | } 67 | 68 | /** 69 | * Get the resource. 70 | * 71 | * @return \JasonLewis\ResourceWatcher\Resource\ResourceInterface 72 | */ 73 | public function getResource() 74 | { 75 | return $this->resource; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pragmarx/tddd", 3 | 4 | "description": "A Self-Hosted TDD Dashboard & Tests Watcher", 5 | 6 | "keywords": [ 7 | "laravel", "ci", "continuous integration", "test", 8 | "testing", "test-driven development", "tdd", "bdd", 9 | "watcher", "phpunit", "dusk", "behat", "phpspec", 10 | "codeception", "atoum", "tester" 11 | ], 12 | 13 | "license": "MIT", 14 | 15 | "authors": [ 16 | { 17 | "name": "Antonio Carlos Ribeiro", 18 | "email": "acr@antoniocarlosribeiro.com", 19 | "role": "Creator & Designer" 20 | } 21 | ], 22 | 23 | "require": { 24 | "php": ">=7.0", 25 | "laravel/framework": ">=5.5", 26 | "pragmarx/support": "~0.8", 27 | "sensiolabs/ansi-to-html": "~1", 28 | "symfony/process": "~3", 29 | "guzzlehttp/guzzle": "~6.3", 30 | "jolicode/jolinotif": "~1.2", 31 | "doctrine/dbal": "~2.5", 32 | "symfony/yaml": "~3.2", 33 | "pusher/pusher-php-server": "~3.0" 34 | }, 35 | 36 | "require-dev": { 37 | "phpunit/phpunit": "^6.5", 38 | "orchestra/testbench": "~3.5" 39 | }, 40 | 41 | "autoload": { 42 | "psr-4": { 43 | "PragmaRX\\Tddd\\Package\\": "src/package", 44 | "PragmaRX\\Tddd\\Tests\\": "tests/" 45 | }, 46 | "psr-0": { 47 | "JasonLewis\\ResourceWatcher": "src/package/Support/jasonlewis/resource-watcher/src" 48 | }, 49 | "files": [ 50 | "src/package/Support/helpers.php" 51 | ] 52 | }, 53 | 54 | "extra": { 55 | "component": "package", 56 | "laravel": { 57 | "providers": [ 58 | "PragmaRX\\Tddd\\Package\\ServiceProvider" 59 | ] 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/package/Data/Models/Test.php: -------------------------------------------------------------------------------- 1 | path, $this->name]); 27 | } 28 | 29 | /** 30 | * Suite relation. 31 | * 32 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 33 | */ 34 | public function suite() 35 | { 36 | return $this->belongsTo('PragmaRX\Tddd\Package\Data\Models\Suite'); 37 | } 38 | 39 | /** 40 | * Get the test command. 41 | * 42 | * @param $value 43 | * 44 | * @return string 45 | */ 46 | public function getTestCommandAttribute($value) 47 | { 48 | $command = $this->suite->testCommand; 49 | 50 | return $command.' '.$this->fullPath; 51 | } 52 | 53 | /** 54 | * Runs relation. 55 | * 56 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 57 | */ 58 | public function runs() 59 | { 60 | return $this->hasMany('PragmaRX\Tddd\Package\Data\Models\Run'); 61 | } 62 | 63 | /** 64 | * Update test sha1. 65 | */ 66 | public function updateSha1() 67 | { 68 | $this->sha1 = @sha1_file($this->fullPath); 69 | 70 | $this->save(); 71 | } 72 | 73 | /** 74 | * Check if the sha1 changed. 75 | * 76 | * @return bool 77 | */ 78 | public function sha1Changed() 79 | { 80 | return $this->sha1 !== @sha1_file($this->fullPath); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/resources/views/dashboard.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ config('tddd.root.names.dashboard') }} 6 | 7 | 8 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 22 | 23 |
24 |
25 |
26 | 29 | 30 |
31 |
32 |
33 | 34 |
35 |
36 |
37 |
38 |
39 |
40 | 41 | 44 | 45 | @if(config('app.env') == 'local') 46 | 47 | @endif 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/routes/web.php: -------------------------------------------------------------------------------- 1 | config('tddd.routes.prefixes.dashboard')], function () { 6 | Route::get('/', ['as' => 'tests-watcher.dashboard', 'uses' => 'Dashboard@index']); 7 | 8 | Route::get('/data', ['as' => 'tests-watcher.dashboard.data', 'uses' => 'Dashboard@data']); 9 | }); 10 | 11 | Route::group(['prefix' => config('tddd.routes.prefixes.tests')], function () { 12 | Route::get('/reset/{project_id}', ['as' => 'tests-watcher.tests.reset', 'uses' => 'Tests@reset']); 13 | 14 | Route::get('/run/{test_id?}', ['as' => 'tests-watcher.tests.run', 'uses' => 'Tests@run']); 15 | 16 | Route::get('/{project_id}/{test_id}/enable/{enable}', ['as' => 'tests-watcher.tests.enable', 'uses' => 'Tests@enable']); 17 | }); 18 | 19 | Route::group(['prefix' => config('tddd.routes.prefixes.projects')], function () { 20 | Route::get('/{project_id}/enable/{enable}', ['as' => 'tests-watcher.projects.enable', 'uses' => 'Projects@enable']); 21 | 22 | Route::get('/{project_id}/notify', ['as' => 'tests-watcher.tests.notify', 'uses' => 'Projects@notify']); 23 | 24 | Route::post('/reset', ['as' => 'tests-watcher.projects.reset', 'uses' => 'Projects@reset']); 25 | 26 | Route::post('/run', ['as' => 'tests-watcher.projects.run.all', 'uses' => 'Projects@run']); 27 | 28 | Route::get('/toggle-all', ['as' => 'tests-watcher.projects.toggle-all', 'uses' => 'Projects@toggleAll']); 29 | }); 30 | 31 | Route::group(['prefix' => config('tddd.routes.prefixes.files')], function () { 32 | Route::get('/edit/{filename}/{suite_id}/{line?}', ['as' => 'tests-watcher.file.edit', 'uses' => 'Files@editFile']); 33 | 34 | Route::get('/{filename}/download', ['as' => 'tests-watcher.image.download', 'uses' => 'Files@imageDownload']); 35 | }); 36 | 37 | Route::group(['prefix' => config('tddd.routes.prefixes.html')], function () { 38 | Route::get('/', ['as' => 'tests-watcher.html.view', 'uses' => 'Html@view']); 39 | }); 40 | -------------------------------------------------------------------------------- /src/package/Data/Models/Suite.php: -------------------------------------------------------------------------------- 1 | project->tests_full_path, 35 | $this->tests_path, 36 | ] 37 | ); 38 | } 39 | 40 | /** 41 | * Project relation. 42 | * 43 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 44 | */ 45 | public function project() 46 | { 47 | return $this->belongsTo('PragmaRX\Tddd\Package\Data\Models\Project'); 48 | } 49 | 50 | /** 51 | * Tester relation. 52 | * 53 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 54 | */ 55 | public function tester() 56 | { 57 | return $this->belongsTo('PragmaRX\Tddd\Package\Data\Models\Tester'); 58 | } 59 | 60 | /** 61 | * Tests relation. 62 | * 63 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 64 | */ 65 | public function tests() 66 | { 67 | return $this->hasMany('PragmaRX\Tddd\Package\Data\Models\Test'); 68 | } 69 | 70 | /** 71 | * Get the test command. 72 | * 73 | * @return mixed 74 | */ 75 | public function getTestCommandAttribute() 76 | { 77 | $command = $this->tester->command.' '.$this->command_options; 78 | 79 | return replace_suite_paths($this, $command); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/resources/assets/sass/_variables.scss: -------------------------------------------------------------------------------- 1 | // Brands 2 | $brand-primary: #2654b2; 3 | $brand-info: #4d80b2; 4 | $brand-success: #6DA34D; 5 | $brand-warning: #EA844D; 6 | $brand-danger: #931621; 7 | $brand-clear: #fdfdfd; 8 | $brand-secondary: #e9ecee; 9 | $brand-gray: #e9ecee; 10 | $brand-bluish: #73dbff; 11 | $brand-darker: #444444; 12 | $brand-darkest: #000000; 13 | $brand-html: #220e03; 14 | $brand-pill: #fffa00; 15 | $brand-white: #fff; 16 | 17 | $body-bg: $brand-clear; 18 | $brand-gray-darker: darken($brand-gray, 10%); 19 | $brand-secondary-darker: darken($brand-secondary, 10%); 20 | $brand-pill-darker: darken($brand-pill, 2%); 21 | $brand-clear-darker: darken($brand-clear, 2%); 22 | $brand-white-darker: darken($brand-white, 2%); 23 | 24 | // Brand lighter 25 | $brand-primary-light: lighten($brand-primary, 20%); 26 | $brand-info-light: lighten($brand-info, 20%); 27 | $brand-success-light: lighten($brand-success, 20%); 28 | $brand-warning-light: lighten($brand-warning, 20%); 29 | $brand-danger-light: lighten($brand-danger, 30%); 30 | $brand-clear-light: lighten($brand-clear, 20%); 31 | $brand-gray-light: lighten($brand-gray, 20%); 32 | $brand-bluish-light: lighten($brand-bluish, 20%); 33 | $brand-darker-light: lighten($brand-darker, 40%); 34 | $brand-darkest-light: lighten($brand-darkest, 20%); 35 | $brand-pill-light: lighten($brand-pill, 20%); 36 | 37 | $brand-failed: $brand-danger-light; 38 | $brand-passed: $brand-success-light; 39 | 40 | // Typography 41 | $icon-font-path: "~bootstrap-sass/assets/fonts/bootstrap/"; 42 | $font-family-sans-serif: "Arsenal", sans-serif; 43 | $font-size-base: 0.9rem; 44 | $line-height-base: 1.6; 45 | $text-color: $brand-darkest; 46 | 47 | // Navbar 48 | $navbar-default-bg: #fff; 49 | 50 | // Buttons 51 | $btn-default-color: $text-color; 52 | 53 | // Inputs 54 | $input-border: lighten($text-color, 40%); 55 | $input-border-focus: lighten($brand-primary, 25%); 56 | $input-color-placeholder: lighten($text-color, 30%); 57 | 58 | // Panels 59 | $panel-default-heading-bg: $brand-clear; 60 | 61 | $fa-font-path: "~font-awesome/fonts"; 62 | 63 | -------------------------------------------------------------------------------- /src/package/Data/Repositories/Support/Runs.php: -------------------------------------------------------------------------------- 1 | $test->id, 28 | 'log' => '', 29 | 'was_ok' => false, 30 | ]); 31 | } 32 | 33 | /** 34 | * Get test info. 35 | * 36 | * @param $test 37 | * 38 | * @return array 39 | */ 40 | protected function getTestInfo($test) 41 | { 42 | $run = Run::where('test_id', $test->id)->orderBy('created_at', 'desc')->first(); 43 | 44 | return [ 45 | 'id' => $test->id, 46 | 'suite_name' => $test->suite->name, 47 | 'project_name' => $test->suite->project->name, 48 | 'project_id' => $test->suite->project->id, 49 | 'path' => $test->path.DIRECTORY_SEPARATOR, 50 | 'name' => $test->name, 51 | 'edit_file_url' => $this->makeEditFileUrl($test), 52 | 'updated_at' => $test->updated_at->diffForHumans(null, false, true), 53 | 'state' => $test->state, 54 | 'enabled' => $test->enabled, 55 | 'editor_name' => $this->getEditor($test->suite)['name'], 56 | 'coverage' => ['enabled' => $test->suite->coverage_enabled, 'index' => $test->suite->coverage_index], 57 | 58 | 'run' => $run, 59 | 'notified_at' => is_null($run) ? null : $run->notified_at, 60 | 'log' => is_null($run) ? null : $run->log, 61 | 'html' => is_null($run) ? null : $run->html, 62 | 'image' => is_null($run) ? null : $run->png, 63 | 'time' => is_null($run) ? '' : (is_null($run->started_at) ? '' : $this->removeBefore($run->started_at->diffForHumans($run->ended_at))), 64 | ]; 65 | } 66 | 67 | /** 68 | * Update the run log. 69 | * 70 | * @param $run 71 | * @param $output 72 | */ 73 | public function updateRunLog($run, $output) 74 | { 75 | $run->log = $output; 76 | 77 | $run->save(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.0.0] - 2018-08-28 4 | ### Added 5 | - Pusher broadcasting 6 | - Ability to disble pooling 7 | ### Breaking Changes 8 | - Add PUSHER_CLUSTER= to your .env file 9 | - Update your root.yml according to config/tddd/root.yml, regarding the pooling key 10 | 11 | ## [0.9.9] - 2017-12-08 12 | ### Added 13 | - Toggle enabled/disabled projects 14 | 15 | ## [0.9.8] - 2017-12-08 16 | ### Added 17 | - Always display log show button 18 | 19 | ## [0.9.7] - 2017-12-07 20 | ### Changed 21 | - Removed clear artisan command 22 | 23 | ## [0.9.6] - 2017-12-07 24 | ### Added 25 | - Tests coverage tab on tests log view 26 | ### Changed 27 | - Config files are now in yaml format 28 | - Renamed package to TDDD - Test Driven Development Dashboard 29 | 30 | ## [0.9.5] - 2017-10-16 31 | ### Added 32 | - MySQL Support 33 | - Option to add environment variables to tester script 34 | - Config for different editors and a default editor 35 | - User can now set one different editor for each suite 36 | - Config for PHPStorm editor 37 | - Config for Sublime Text 3 editor 38 | - Config for Visual Studio Code editor 39 | - Option to disable project on Dashboard 40 | - Projects can now be disbled 41 | - No need to refresh page when rebooting watcher anymore 42 | - Input to filter projects 43 | - Option to configure the poll interval (defaults to 1500ms) 44 | - Show spinner on running project 45 | - Show badge (passed/failed) for each project 46 | - Button to run all tests on all (filtered) projects 47 | - Added AVA tester 48 | - Test state to log modal 49 | - Option to run test from the log modal 50 | - Option to reset state of all projects 51 | - Watch and automatically reload config 52 | - Display tester log in real-time 53 | ### Changed 54 | - Allow better configuration of editor's binary 55 | - Moved Laravel related classes out from Vendor\Laravel 56 | - Completely restructure package directory 57 | - License is now MIT 58 | - Improved regex matcher of editable source files (and lines) 59 | ### Fixed 60 | - Abending when tester used in suite does not exists 61 | - Piper script not being 62 | - Test subfolders not stored correctly 63 | 64 | ## [0.9.4] - 2017-10-11 65 | ### Added 66 | - Show Jest snapshots in dashboard log modal 67 | - Show exclustions in terminal log 68 | 69 | ## [0.9.3] - 2017-10-10 70 | ### Added 71 | - Support for Javascript testing (Jest) 72 | ### Changed 73 | - Ignore abstract PHP classes 74 | 75 | ## [0.9.2] - 2017-09-10 76 | ### Changed 77 | - Bug fixes 78 | 79 | ## [0.9.1] - 2017-08-10 80 | ### Changed 81 | - Bug fixes 82 | 83 | ## [0.9.0] - 2017-07-10 84 | ### Changed 85 | - Complete redesign of dashboard 86 | - Moved from ReactJS to VueJS 87 | 88 | ## [0.5.0] - 2015-03-10 89 | ### Added 90 | - Support Laravel 5 91 | 92 | ## [0.1.0] - 2014-07-06 93 | ### Added 94 | - First version 95 | -------------------------------------------------------------------------------- /src/package/Services/Base.php: -------------------------------------------------------------------------------- 1 | command->{$type}($line); 48 | } 49 | 50 | /** 51 | * Show a comment in terminal. 52 | * 53 | * @param $comment 54 | */ 55 | public function showComment($comment) 56 | { 57 | $this->command->comment($comment); 58 | } 59 | 60 | /** 61 | * Get the event name. 62 | * 63 | * @param $eventCode 64 | * 65 | * @return string 66 | */ 67 | protected function getEventName($eventCode) 68 | { 69 | $event = '(unknown event)'; 70 | 71 | switch ($eventCode) { 72 | case Event::RESOURCE_DELETED: 73 | $event = 'deleted'; 74 | break; 75 | case Event::RESOURCE_CREATED: 76 | $event = 'created'; 77 | break; 78 | case Event::RESOURCE_MODIFIED: 79 | $event = 'modified'; 80 | break; 81 | } 82 | 83 | return $event; 84 | } 85 | 86 | /** 87 | * Display messages in terminal. 88 | * 89 | * @param $messages 90 | */ 91 | protected function displayMessages($messages) 92 | { 93 | $fatal = $messages->reduce(function ($carry, $message) { 94 | $prefix = $message['type'] == 'error' ? 'FATAL ERROR: ' : ''; 95 | 96 | $this->command->{$message['type']}($prefix.$message['body']); 97 | 98 | if ($message['type'] == 'error') { 99 | return true; 100 | } 101 | 102 | return $carry; 103 | }); 104 | 105 | if ($fatal == true) { 106 | die; 107 | } 108 | } 109 | 110 | /** 111 | * Set the command. 112 | * 113 | * @param $command 114 | */ 115 | protected function setCommand($command) 116 | { 117 | $this->command = $command; 118 | 119 | if (!is_null($this->loader)) { 120 | $this->loader->setCommand($this->command); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/package/Services/Config.php: -------------------------------------------------------------------------------- 1 | yaml = $yaml; 32 | } 33 | 34 | /** 35 | * Check if the config is valid. 36 | * 37 | * @return bool 38 | */ 39 | protected function configIsValid() 40 | { 41 | return is_array($this->config) && count($this->config) > 0; 42 | } 43 | 44 | /** 45 | * Get a configuration key. 46 | * 47 | * @param $key 48 | * @param mixed|null $default 49 | * 50 | * @throws \Exception 51 | * 52 | * @return mixed 53 | */ 54 | public function get($key, $default = null) 55 | { 56 | $this->loadConfig(); 57 | 58 | return config("tddd.{$key}", $default); 59 | } 60 | 61 | /** 62 | * Load the config. 63 | */ 64 | public function loadConfig() 65 | { 66 | if ($this->configIsValid()) { 67 | return; 68 | } 69 | 70 | $this->yaml->loadToConfig($this->getConfigPath(), 'tddd', true)->toArray(); 71 | } 72 | 73 | /** 74 | * Force the config to be reloaded. 75 | */ 76 | public function reloadConfig() 77 | { 78 | $this->invalidateConfig(); 79 | 80 | $this->loadConfig(); 81 | } 82 | 83 | /** 84 | * Get a list of all config files. 85 | * 86 | * @return Collection 87 | */ 88 | public function getConfigFiles() 89 | { 90 | return $this->yaml->listFiles($this->getConfigPath())->flatten(); 91 | } 92 | 93 | /** 94 | * Get the config path. 95 | * 96 | * @return \Illuminate\Config\Repository|mixed 97 | */ 98 | public function getConfigPath() 99 | { 100 | if (is_null($this->configPath)) { 101 | $this->configPath = replace_laravel_paths(config('tddd-base.path')); 102 | } 103 | 104 | return $this->configPath; 105 | } 106 | 107 | /** 108 | * Set the config item. 109 | * 110 | * @param $data 111 | */ 112 | public function set($data) 113 | { 114 | $this->config = array_merge($data, $this->config); 115 | 116 | $this->mergeWithLaravelConfig(); 117 | } 118 | 119 | /** 120 | * Invalidate the current config. 121 | */ 122 | public function invalidateConfig() 123 | { 124 | $this->config = []; 125 | } 126 | 127 | /** 128 | * Check if a file is a config file. 129 | * 130 | * @param $file 131 | * 132 | * @return bool 133 | */ 134 | public function isConfigFile($file) 135 | { 136 | return $this->getConfigFiles()->contains($file); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/package/Support/jasonlewis/resource-watcher/src/JasonLewis/ResourceWatcher/Tracker.php: -------------------------------------------------------------------------------- 1 | tracked[$resource->getKey()] = [$resource, $listener]; 27 | } 28 | 29 | /** 30 | * Determine if a resource is tracked. 31 | * 32 | * @param \JasonLewis\ResourceWatcher\Resource\ResourceInterface $resource 33 | */ 34 | public function isTracked(ResourceInterface $resource) 35 | { 36 | return isset($this->tracked[$resource->getKey()]); 37 | } 38 | 39 | /** 40 | * Get the tracked resources. 41 | * 42 | * @return array 43 | */ 44 | public function getTracked() 45 | { 46 | return $this->tracked; 47 | } 48 | 49 | /** 50 | * Detect any changes on the tracked resources. 51 | * 52 | * @return void 53 | */ 54 | public function checkTrackings() 55 | { 56 | foreach ($this->tracked as $name => $tracked) { 57 | list($resource, $listener) = $tracked; 58 | 59 | if (!$events = $resource->detectChanges()) { 60 | continue; 61 | } 62 | 63 | foreach ($events as $event) { 64 | if ($event instanceof Event) { 65 | $this->callListenerBindings($listener, $event); 66 | } 67 | } 68 | } 69 | } 70 | 71 | /** 72 | * Call the bindings on the listener for a given event. 73 | * 74 | * @param \JasonLewis\ResourceWatcher\Listener $listener 75 | * @param \JasonLewis\ResourceWatcher\Event $event 76 | * 77 | * @return void 78 | */ 79 | protected function callListenerBindings(Listener $listener, Event $event) 80 | { 81 | $binding = $listener->determineEventBinding($event); 82 | 83 | if ($listener->hasBinding($binding)) { 84 | foreach ($listener->getBindings($binding) as $callback) { 85 | $resource = $event->getResource(); 86 | 87 | call_user_func($callback, $resource, $resource->getPath()); 88 | } 89 | } 90 | 91 | // If a listener has a binding for anything we'll also spin through 92 | // them and call each of them. 93 | if ($listener->hasBinding('*')) { 94 | foreach ($listener->getBindings('*') as $callback) { 95 | $resource = $event->getResource(); 96 | 97 | call_user_func($callback, $event, $resource, $resource->getPath()); 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/package/Support/jasonlewis/resource-watcher/src/JasonLewis/ResourceWatcher/Resource/DirectoryResource.php: -------------------------------------------------------------------------------- 1 | descendants = $this->detectDirectoryDescendants(); 26 | } 27 | 28 | /** 29 | * Detect any changes to the resource. 30 | * 31 | * @return array 32 | */ 33 | public function detectChanges() 34 | { 35 | $events = parent::detectChanges(); 36 | 37 | // When a descendant file is created or deleted a modified event is fired on the 38 | // directory. This is the only way a directory will receive a modified event and 39 | // will thus result in two events being fired for a single descendant modification 40 | // within the directory. This will clear the events if we got a modified event. 41 | if ($events and $events[0]->getCode() == Event::RESOURCE_MODIFIED) { 42 | $events = []; 43 | } 44 | 45 | foreach ($this->descendants as $key => $descendant) { 46 | $descendantEvents = $descendant->detectChanges(); 47 | 48 | foreach ($descendantEvents as $event) { 49 | if ($event instanceof Event and $event->getCode() == Event::RESOURCE_DELETED) { 50 | unset($this->descendants[$key]); 51 | } 52 | } 53 | 54 | $events = array_merge($events, $descendantEvents); 55 | } 56 | 57 | // If this directory still exists we'll check the directories descendants again for any 58 | // new descendants. 59 | if ($this->exists) { 60 | foreach ($this->detectDirectoryDescendants() as $key => $descendant) { 61 | if (!isset($this->descendants[$key])) { 62 | $this->descendants[$key] = $descendant; 63 | 64 | $events[] = new Event($descendant, Event::RESOURCE_CREATED); 65 | } 66 | } 67 | } 68 | 69 | return $events; 70 | } 71 | 72 | /** 73 | * Detect the descendant resources of the directory. 74 | * 75 | * @return array 76 | */ 77 | protected function detectDirectoryDescendants() 78 | { 79 | $descendants = []; 80 | 81 | foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->getPath())) as $file) { 82 | if ($file->isDir() and !in_array($file->getBasename(), ['.', '..'])) { 83 | $resource = new self($file, $this->files); 84 | 85 | $descendants[$resource->getKey()] = $resource; 86 | } elseif ($file->isFile()) { 87 | $resource = new FileResource($file, $this->files); 88 | 89 | $descendants[$resource->getKey()] = $resource; 90 | } 91 | } 92 | 93 | return $descendants; 94 | } 95 | 96 | /** 97 | * Get the descendants of the directory. 98 | * 99 | * @return array 100 | */ 101 | public function getDescendants() 102 | { 103 | return $this->descendants; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/package/Data/Repositories/Support/Queue.php: -------------------------------------------------------------------------------- 1 | state == Constants::STATE_QUEUED 22 | && 23 | QueueModel::where('test_id', $test->id)->first(); 24 | } 25 | 26 | /** 27 | * Queue all tests. 28 | */ 29 | public function queueAllTests() 30 | { 31 | $this->showProgress('QUEUE: adding tests to queue...'); 32 | 33 | foreach (Test::all() as $test) { 34 | $this->addTestToQueue($test); 35 | } 36 | } 37 | 38 | /** 39 | * Queue all tests from a particular suite. 40 | * 41 | * @param $suite_id 42 | */ 43 | public function queueTestsForSuite($suite_id) 44 | { 45 | $tests = Test::where('suite_id', $suite_id)->get(); 46 | 47 | foreach ($tests as $test) { 48 | $this->addTestToQueue($test); 49 | } 50 | } 51 | 52 | /** 53 | * Add a test to the queue. 54 | * 55 | * @param $test 56 | * @param bool $force 57 | */ 58 | public function addTestToQueue($test, $force = false) 59 | { 60 | if ($test->enabled && $test->suite->project->enabled && !$this->isEnqueued($test)) { 61 | $test->updateSha1(); 62 | 63 | QueueModel::updateOrCreate(['test_id' => $test->id]); 64 | 65 | // After queueing, if it's the only one, it may take the test and run it right away, 66 | // so we must wait a little for it to happen 67 | sleep(1); 68 | 69 | // We then get a fresh model, which may have a different state now 70 | $test = $test->fresh(); 71 | 72 | if ($force || !in_array($test->state, [Constants::STATE_RUNNING, Constants::STATE_QUEUED])) { 73 | $test->state = Constants::STATE_QUEUED; 74 | 75 | $test->timestamps = false; 76 | 77 | $test->save(); 78 | } 79 | } 80 | } 81 | 82 | /** 83 | * Get a test from the queue. 84 | * 85 | * @return \PragmaRX\Tddd\Package\Data\Models\Test|null 86 | */ 87 | public function getNextTestFromQueue() 88 | { 89 | $query = QueueModel::join('tddd_tests', 'tddd_tests.id', '=', 'tddd_queue.test_id') 90 | ->where('tddd_tests.enabled', true) 91 | ->where('tddd_tests.state', '!=', Constants::STATE_RUNNING); 92 | 93 | if (!$queue = $query->first()) { 94 | return; 95 | } 96 | 97 | return $queue->test; 98 | } 99 | 100 | /** 101 | * Remove test from que run queue. 102 | * 103 | * @param $test 104 | * 105 | * @return mixed 106 | */ 107 | protected function removeTestFromQueue($test) 108 | { 109 | QueueModel::where('test_id', $test->id)->delete(); 110 | 111 | return $test; 112 | } 113 | 114 | /** 115 | * Reset a test to idle state. 116 | * 117 | * @param $test 118 | */ 119 | protected function resetTest($test) 120 | { 121 | QueueModel::where('test_id', $test->id)->delete(); 122 | 123 | $test->state = Constants::STATE_IDLE; 124 | 125 | $test->timestamps = false; 126 | 127 | $test->save(); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/package/Support/jasonlewis/resource-watcher/src/JasonLewis/ResourceWatcher/Resource/FileResource.php: -------------------------------------------------------------------------------- 1 | resource = $resource; 57 | $this->path = $resource->getRealPath(); 58 | $this->files = $files; 59 | $this->exists = $this->files->exists($this->path); 60 | $this->lastModified = !$this->exists ?: $this->files->lastModified($this->path); 61 | } 62 | 63 | /** 64 | * Detect any changes to the resource. 65 | * 66 | * @return array 67 | */ 68 | public function detectChanges() 69 | { 70 | clearstatcache(true, $this->path); 71 | 72 | if (!$this->exists and $this->files->exists($this->path)) { 73 | $this->lastModified = $this->files->lastModified($this->path); 74 | $this->exists = true; 75 | 76 | return [new Event($this, Event::RESOURCE_CREATED)]; 77 | } elseif ($this->exists and !$this->files->exists($this->path)) { 78 | $this->exists = false; 79 | 80 | return [new Event($this, Event::RESOURCE_DELETED)]; 81 | } elseif ($this->exists and $this->isModified()) { 82 | $this->lastModified = $this->files->lastModified($this->path); 83 | 84 | return [new Event($this, Event::RESOURCE_MODIFIED)]; 85 | } 86 | 87 | return []; 88 | } 89 | 90 | /** 91 | * Determine if the resource has been modified. 92 | * 93 | * @return bool 94 | */ 95 | public function isModified() 96 | { 97 | return $this->lastModified < $this->files->lastModified($this->path); 98 | } 99 | 100 | /** 101 | * Get the resource key. 102 | * 103 | * @return string 104 | */ 105 | public function getKey() 106 | { 107 | return md5($this->path); 108 | } 109 | 110 | /** 111 | * Get the path of the resource. 112 | * 113 | * @return string 114 | */ 115 | public function getPath() 116 | { 117 | return $this->path; 118 | } 119 | 120 | /** 121 | * Get the resource SplFileInfo. 122 | * 123 | * @return \SplFileInfo 124 | */ 125 | public function getSplFileInfo() 126 | { 127 | return $this->resource; 128 | } 129 | 130 | /** 131 | * Get the resources last modified timestamp. 132 | * 133 | * @return int 134 | */ 135 | public function getLastModified() 136 | { 137 | return $this->lastModified; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/package/Support/jasonlewis/resource-watcher/src/JasonLewis/ResourceWatcher/Watcher.php: -------------------------------------------------------------------------------- 1 | tracker = $tracker; 46 | $this->files = $files; 47 | } 48 | 49 | /** 50 | * Register a resource to be watched. 51 | * 52 | * @param string $resource 53 | * 54 | * @return \JasonLewis\ResourceWatcher\Listener 55 | */ 56 | public function watch($resource) 57 | { 58 | if (!$this->files->exists($resource)) { 59 | throw new RuntimeException('Resource must exist before you can watch it.'); 60 | } elseif ($this->files->isDirectory($resource)) { 61 | $resource = new DirectoryResource(new SplFileInfo($resource), $this->files); 62 | 63 | $resource->setupDirectory(); 64 | } else { 65 | $resource = new FileResource(new SplFileInfo($resource), $this->files); 66 | } 67 | 68 | // The listener gives users the ability to bind listeners on the events 69 | // created when watching a file or directory. We'll give the listener 70 | // to the tracker so the tracker can fire any bound listeners. 71 | $listener = new Listener(); 72 | 73 | $this->tracker->register($resource, $listener); 74 | 75 | return $listener; 76 | } 77 | 78 | /** 79 | * Start watching for a given interval. The interval and timeout and measured 80 | * in microseconds, so 1,000,000 microseconds is equal to 1 second. 81 | * 82 | * @param int $interval 83 | * @param int $timeout 84 | * @param \Closure $callback 85 | * 86 | * @return void 87 | */ 88 | public function startWatch($interval = 1000000, $timeout = null, Closure $callback = null) 89 | { 90 | $this->watching = true; 91 | 92 | $timeWatching = 0; 93 | 94 | while ($this->watching) { 95 | if (is_callable($callback)) { 96 | call_user_func($callback, $this); 97 | } 98 | 99 | usleep($interval); 100 | 101 | $this->tracker->checkTrackings(); 102 | 103 | $timeWatching += $interval; 104 | 105 | if (!is_null($timeout) and $timeWatching >= $timeout) { 106 | $this->stopWatch(); 107 | } 108 | } 109 | } 110 | 111 | /** 112 | * Alias of startWatch. 113 | * 114 | * @param int $interval 115 | * @param int $timeout 116 | * @param \Closure $callback 117 | * 118 | * @return void 119 | */ 120 | public function start($interval = 1000000, $timeout = null, Closure $callback = null) 121 | { 122 | $this->startWatch($interval, $timeout, $callback); 123 | } 124 | 125 | /** 126 | * Get the tracker instance. 127 | * 128 | * @return \JasonLewis\ResourceWatcher\Tracker 129 | */ 130 | public function getTracker() 131 | { 132 | return $this->tracker; 133 | } 134 | 135 | /** 136 | * Stop watching. 137 | * 138 | * @return void 139 | */ 140 | public function stopWatch() 141 | { 142 | $this->watching = false; 143 | } 144 | 145 | /** 146 | * Alias of stopWatch. 147 | * 148 | * @return void 149 | */ 150 | public function stop() 151 | { 152 | $this->stopWatch(); 153 | } 154 | 155 | /** 156 | * Determine if watcher is watching. 157 | * 158 | * @return bool 159 | */ 160 | public function isWatching() 161 | { 162 | return $this->watching; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/package/Support/jasonlewis/resource-watcher/README.md: -------------------------------------------------------------------------------- 1 | # Resource Watcher 2 | 3 | A resource watcher allows you to watch a resource for any changes. This means you can watch a directory and then listen for any changes to files within that directory or to the directory itself. 4 | 5 | [![Build Status](https://travis-ci.org/jasonlewis/resource-watcher.png?branch=master)](https://travis-ci.org/jasonlewis/resource-watcher) 6 | 7 | ## Installation 8 | 9 | To install Resource Watcher add it to the `requires` key of your `composer.json` file. 10 | 11 | ``` 12 | "jasonlewis/resource-watcher": "1.1.*" 13 | ``` 14 | 15 | Then update your project with `composer update`. 16 | 17 | ## Usage 18 | 19 | The Resource Watcher is best used from a console. An example of a console command can be found in the `watcher` file. This file is commented to give you 20 | an idea of how to configure and use a resource watcher. Once you've customized the command to your liking you can run it from your console. 21 | 22 | ``` 23 | $ php watcher 24 | ``` 25 | 26 | Any changes you make to the resource will be outputted to the console. 27 | 28 | ## Quick Overview 29 | 30 | To watch resources you first need an instance of `JasonLewis\ResourceWatcher\Watcher`. This class has a few dependencies (`JasonLewis\ResourceWatcher\Tracker` and `Illuminate\Filesystem\Filesystem`) that must also be instantiated. 31 | 32 | ```php 33 | $files = new Illuminate\Filesystem\Filesystem; 34 | $tracker = new JasonLewis\ResourceWatcher\Tracker; 35 | 36 | $watcher = new JasonLewis\ResourceWatcher\Watcher($tracker, $files); 37 | ``` 38 | 39 | Now that we have our watcher we can create a listener for a given resource. 40 | 41 | ```php 42 | $listener = $watcher->watch('path/to/resource'); 43 | ``` 44 | 45 | When you watch a resource an instance of `JasonLewis\ResourceWatcher\Listener` is returned. With this we can now listen for certain events on a resource. 46 | 47 | There are three events we can listen for: `modify`, `create`, and `delete`. The callback you give to the listener receives two parameters, the first being an implementation of `JasonLewis\ResourceWatcher\Resource\ResourceInterface` and the second being the absolute path to the resource. 48 | 49 | ```php 50 | $listener->modify(function($resource, $path) 51 | { 52 | echo "{$path} has been modified.".PHP_EOL; 53 | }); 54 | ``` 55 | 56 | You can use the alias methods as well. 57 | 58 | ```php 59 | $listener->onModify(function($resource, $path) 60 | { 61 | echo "{$path} has been modified.".PHP_EOL; 62 | }); 63 | ``` 64 | 65 | You can also listen for any of these events. This time the callback receives a different set of parameters, the first being an instance of `JasonLewis\ResourceWatcher\Event` and the remaining two being the same as before. 66 | 67 | ```php 68 | $listener->anything(function($event, $resource, $path) 69 | { 70 | 71 | }); 72 | ``` 73 | 74 | > Remember that each call to `$watcher->watch()` will return an instance of `JasonLewis\ResourceWatcher\Listener`, so be sure you attach listeners to the right one! 75 | 76 | Once you're watching some resources and have your listeners set up you can start the watching process. 77 | 78 | ```php 79 | $watcher->start(); 80 | ``` 81 | 82 | By default the watcher will poll for changes every second. You can adjust this by passing in an optional first parameter to the `start` method. The polling interval is given in microseconds, so 1,000,000 microseconds is 1 second. The watch will continue until such time that it's aborted from the console. To set a timeout pass in the number of microseconds before the watch will abort as the second parameter. 83 | 84 | The `start` method can also be given a callback as an optional third parameter. This callback will be fired before checking for any changes to resources. 85 | 86 | ```php 87 | $watcher->start(1000000, null, function($watcher) 88 | { 89 | // Perhaps perform some other check and then stop the watch. 90 | $watcher->stop(); 91 | }); 92 | ``` 93 | 94 | ## Framework Integration 95 | 96 | ### Laravel 4 97 | 98 | Included is a service provider for the Laravel 4 framework. This service provider will bind an instance of `JasonLewis\ResourceWatcher\Watcher` to the application container under the `watcher` key. 99 | 100 | ```php 101 | $listener = $app['watcher']->watch('path/to/resource'); 102 | 103 | // Or if you don't have access to an instance of the application container. 104 | $listener = app('tddd.watcher')->watch('path/to/resource'); 105 | ``` 106 | 107 | Register `JasonLewis\ResourceWatcher\Integration\LaravelServiceProvider` in the array of providers in `app/config/app.php`. 108 | 109 | ## License 110 | 111 | Resource Watcher is released under the 2-clause BSD license. See the `LICENSE` for more details. 112 | -------------------------------------------------------------------------------- /src/package/Support/jasonlewis/resource-watcher/src/JasonLewis/ResourceWatcher/Listener.php: -------------------------------------------------------------------------------- 1 | registerBinding($event, $callback); 34 | } 35 | 36 | /** 37 | * Bind to anything. 38 | * 39 | * @param \Closure $callback 40 | * 41 | * @return void 42 | */ 43 | public function onAnything(Closure $callback) 44 | { 45 | $this->on('*', $callback); 46 | } 47 | 48 | /** 49 | * Alias of the onAnything event. 50 | * 51 | * @param \Closure $callback 52 | * 53 | * @return void 54 | */ 55 | public function anything(Closure $callback) 56 | { 57 | $this->on('*', $callback); 58 | } 59 | 60 | /** 61 | * Bind to a modify event. 62 | * 63 | * @param \Closure $callback 64 | * 65 | * @return void 66 | */ 67 | public function onModify(Closure $callback) 68 | { 69 | $this->on('modify', $callback); 70 | } 71 | 72 | /** 73 | * Alias of the onModify method. 74 | * 75 | * @param \Closure $callback 76 | * 77 | * @return void 78 | */ 79 | public function modify(Closure $callback) 80 | { 81 | $this->on('modify', $callback); 82 | } 83 | 84 | /** 85 | * Bind to a delete event. 86 | * 87 | * @param \Closure $callback 88 | * 89 | * @return void 90 | */ 91 | public function onDelete(Closure $callback) 92 | { 93 | $this->on('delete', $callback); 94 | } 95 | 96 | /** 97 | * Alias of the onDelete method. 98 | * 99 | * @param \Closure $callback 100 | * 101 | * @return void 102 | */ 103 | public function delete(Closure $callback) 104 | { 105 | $this->on('delete', $callback); 106 | } 107 | 108 | /** 109 | * Bind to a create event. 110 | * 111 | * @param \Closure $callback 112 | * 113 | * @return void 114 | */ 115 | public function onCreate(Closure $callback) 116 | { 117 | $this->on('create', $callback); 118 | } 119 | 120 | /** 121 | * Alias of the onCreate method. 122 | * 123 | * @param \Closure $callback 124 | * 125 | * @return void 126 | */ 127 | public function create(Closure $callback) 128 | { 129 | $this->on('create', $callback); 130 | } 131 | 132 | /** 133 | * Register a binding. 134 | * 135 | * @param string $binding 136 | * @param \Closure $callback 137 | * 138 | * @return void 139 | */ 140 | protected function registerBinding($binding, Closure $callback) 141 | { 142 | $this->bindings[$binding][] = $callback; 143 | } 144 | 145 | /** 146 | * Determine if a binding is bound to the listener. 147 | * 148 | * @param string $binding 149 | * 150 | * @return bool 151 | */ 152 | public function hasBinding($binding) 153 | { 154 | return isset($this->bindings[$binding]); 155 | } 156 | 157 | /** 158 | * Get the bindings or a specific array of bindings. 159 | * 160 | * @param string $binding 161 | * 162 | * @return array 163 | */ 164 | public function getBindings($binding = null) 165 | { 166 | if (is_null($binding)) { 167 | return $this->bindings; 168 | } 169 | 170 | return $this->bindings[$binding]; 171 | } 172 | 173 | /** 174 | * Determine the binding for a given event. 175 | * 176 | * @param \JasonLewis\ResourceWatcher\Event $event 177 | * 178 | * @return string 179 | */ 180 | public function determineEventBinding(Event $event) 181 | { 182 | switch ($event->getCode()) { 183 | case Event::RESOURCE_DELETED: 184 | return 'delete'; 185 | break; 186 | case Event::RESOURCE_CREATED: 187 | return 'create'; 188 | break; 189 | case Event::RESOURCE_MODIFIED: 190 | return 'modify'; 191 | break; 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/package/Services/Loader.php: -------------------------------------------------------------------------------- 1 | dataRepository = $dataRepository; 39 | } 40 | 41 | /** 42 | * Create or update the suite. 43 | * 44 | * @param $suite_name 45 | * @param $project 46 | * @param $suite_data 47 | */ 48 | private function createSuite($suite_name, $project, $suite_data) 49 | { 50 | $this->showProgress(" -- suite '{$suite_name}'"); 51 | 52 | if (!$this->dataRepository->createOrUpdateSuite($suite_name, $project->id, $suite_data)) { 53 | $this->displayMessages($this->dataRepository->getMessages()); 54 | die; 55 | } 56 | } 57 | 58 | /** 59 | * Read configuration and load testers, projects, suites... 60 | */ 61 | public function loadEverything($showTests = false) 62 | { 63 | $this->showProgress('Config loaded from '.Config::getConfigPath()); 64 | 65 | $this->loadTesters(); 66 | 67 | $this->loadProjects(); 68 | 69 | $this->loadTests($showTests); 70 | } 71 | 72 | /** 73 | * Load all testers to database. 74 | */ 75 | public function loadTesters() 76 | { 77 | $this->showProgress('Loading testers...', 'info'); 78 | 79 | if (!is_arrayable($testers = $this->config('testers')) or count($testers) == 0) { 80 | $this->showProgress('No testers found.', 'error'); 81 | 82 | return; 83 | } 84 | 85 | foreach ($testers as $data) { 86 | $this->showProgress("TESTER: {$data['name']}"); 87 | 88 | $this->dataRepository->createOrUpdateTester($data); 89 | } 90 | 91 | $this->dataRepository->deleteMissingTesters(array_keys($testers)); 92 | } 93 | 94 | /** 95 | * Load all projects to database. 96 | */ 97 | public function loadProjects() 98 | { 99 | $this->showProgress('Loading projects and suites...', 'info'); 100 | 101 | if (!is_arrayable($projects = $this->config('projects')) or count($projects) == 0) { 102 | $this->showProgress('No projects found.', 'error'); 103 | 104 | return; 105 | } 106 | 107 | foreach ($projects as $data) { 108 | $this->showProgress("Project '{$data['name']}'", 'comment'); 109 | 110 | $project = $this->dataRepository->createOrUpdateProject($data['name'], $data['path'], $data['tests_path']); 111 | 112 | $this->refreshProjectSuites($data, $project); 113 | 114 | $this->addToWatchFolders($data['path'], $data['watch_folders']); 115 | 116 | $this->addToExclusions($data['path'], $data['exclude']); 117 | } 118 | 119 | $this->dataRepository->deleteMissingProjects(collect($this->config('projects'))->pluck('name')->toArray()); 120 | } 121 | 122 | /** 123 | * Load all test files to database. 124 | */ 125 | public function loadTests($showTests) 126 | { 127 | $this->showProgress('Loading tests...', 'info'); 128 | 129 | $this->dataRepository->syncTests($this->exclusions, $showTests); 130 | 131 | $this->displayMessages($this->dataRepository->getMessages()); 132 | } 133 | 134 | /** 135 | * Add folders to the watch list. 136 | * 137 | * @param $path 138 | * @param $watch_folders 139 | */ 140 | public function addToWatchFolders($path, $watch_folders) 141 | { 142 | collect($watch_folders)->each(function ($folder) use ($path) { 143 | $this->watchFolders[] = !file_exists($new = make_path([$path, $folder])) && file_exists($folder) 144 | ? $folder 145 | : $new; 146 | }); 147 | } 148 | 149 | /** 150 | * Add path to exclusions list. 151 | * 152 | * @param $path 153 | * @param $exclude 154 | */ 155 | public function addToExclusions($path, $exclude) 156 | { 157 | collect($exclude)->each(function ($folder) use ($path) { 158 | $this->exclusions[] = $excluded = make_path([$path, $folder]); 159 | 160 | $this->showProgress("EXCLUDED: {$excluded}"); 161 | }); 162 | } 163 | 164 | /** 165 | * Refresh all suites for a project. 166 | * 167 | * @param $data 168 | * @param $project 169 | */ 170 | private function refreshProjectSuites($data, $project) 171 | { 172 | $this->dataRepository->removeMissingSuites($suites = $data['suites'], $project); 173 | 174 | collect($suites)->map(function ($data, $name) use ($project) { 175 | $this->createSuite($name, $project, $data); 176 | }); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/package/Data/Repositories/Support/Suites.php: -------------------------------------------------------------------------------- 1 | id : $project_id; 24 | 25 | if (is_null($tester = Tester::where('name', $suite_data['tester'])->first())) { 26 | $this->addMessage("Tester {$suite_data['tester']} not found.", 'error'); 27 | 28 | return false; 29 | } 30 | 31 | return Suite::updateOrCreate( 32 | [ 33 | 'name' => $name, 34 | 'project_id' => $project_id, 35 | ], 36 | [ 37 | 'tester_id' => $tester->id, 38 | 'tests_path' => array_get($suite_data, 'tests_path'), 39 | 'command_options' => array_get($suite_data, 'command_options'), 40 | 'file_mask' => array_get($suite_data, 'file_mask'), 41 | 'retries' => array_get($suite_data, 'retries'), 42 | 'editor' => array_get($suite_data, 'editor'), 43 | 'coverage_enabled' => array_get($suite_data, 'coverage.enabled', false), 44 | 'coverage_index' => array_get($suite_data, 'coverage.index'), 45 | ] 46 | ); 47 | } 48 | 49 | /** 50 | * Find suite by project and name. 51 | * 52 | * @param $name 53 | * @param $project_id 54 | * 55 | * @return \PragmaRX\Tddd\Package\Data\Models\Suite|null 56 | */ 57 | public function findSuiteByNameAndProject($name, $project_id) 58 | { 59 | return Suite::where('name', $name) 60 | ->where('project_id', $project_id) 61 | ->first(); 62 | } 63 | 64 | /** 65 | * Get all suites. 66 | * 67 | * @return \Illuminate\Database\Eloquent\Collection|static[] 68 | */ 69 | public function getSuites() 70 | { 71 | return Suite::all(); 72 | } 73 | 74 | /** 75 | * Find suite by id. 76 | * 77 | * @return \PragmaRX\Tddd\Package\Data\Models\Suite|null 78 | */ 79 | public function findSuiteById($id) 80 | { 81 | return Suite::find($id); 82 | } 83 | 84 | /** 85 | * Remove suites that are not in present in config. 86 | * 87 | * @param $suites 88 | * @param $project 89 | */ 90 | public function removeMissingSuites($suites, $project) 91 | { 92 | Suite::where('project_id', $project->id)->whereNotIn('name', collect($suites)->keys())->each(function ($suite) { 93 | $suite->delete(); 94 | }); 95 | } 96 | 97 | /** 98 | * Sync all tests for a particular suite. 99 | * 100 | * @param $suite 101 | * @param $exclusions 102 | */ 103 | protected function syncSuiteTests($suite, $exclusions, $showTests) 104 | { 105 | $files = $this->getAllFilesFromSuite($suite); 106 | 107 | foreach ($files as $file) { 108 | if (!$this->isExcluded($exclusions, null, $file) && $this->isTestable($file->getRealPath())) { 109 | $this->createOrUpdateTest($file, $suite); 110 | 111 | if ($showTests) { 112 | $this->addMessage('NEW TEST: '.$file->getRealPath()); 113 | } 114 | } else { 115 | // If the test already exists, delete it. 116 | // 117 | if ($test = $this->findTestByNameAndSuite($file, $suite)) { 118 | $test->delete(); 119 | } 120 | } 121 | } 122 | 123 | foreach ($suite->tests as $test) { 124 | if (!file_exists($path = $test->fullPath)) { 125 | $test->delete(); 126 | } 127 | } 128 | } 129 | 130 | /** 131 | * Get all files from a suite. 132 | * 133 | * @param $suite 134 | * 135 | * @return array 136 | */ 137 | protected function getAllFilesFromSuite($suite) 138 | { 139 | if (!file_exists($suite->testsFullPath)) { 140 | die('FATAL ERROR: directory not found: '.$suite->testsFullPath.'.'); 141 | } 142 | 143 | $files = Finder::create()->files()->in($suite->testsFullPath); 144 | 145 | if ($suite->file_mask) { 146 | $files->name($suite->file_mask); 147 | } 148 | 149 | return iterator_to_array($files, false); 150 | } 151 | 152 | /** 153 | * Get all suites for a path. 154 | * 155 | * @param $path 156 | * 157 | * @return mixed 158 | */ 159 | public function getSuitesForPath($path) 160 | { 161 | $projects = $this->getProjects(); 162 | 163 | // Reduce the collection of projects by those whose path properties 164 | // (should be only 1) are contained in the fullpath of our 165 | // changed file 166 | $filtered_projects = $projects->filter(function ($project) use ($path) { 167 | return substr_count($path, $project->path) > 0; 168 | }); 169 | 170 | // Get filtered projects dependencies 171 | $depends = $projects->filter(function ($project) use ($filtered_projects) { 172 | if (!is_null($depends = config("tddd.projects.{$project->name}.depends"))) { 173 | return collect($depends)->filter(function ($item) use ($filtered_projects) { 174 | return !is_null($filtered_projects->where('name', $item)->first()); 175 | }); 176 | } 177 | 178 | return false; 179 | }); 180 | 181 | // At this point we have (hopefully only 1) project. Now we need 182 | // the suite(s) associated with the project. 183 | return Suite::whereIn('project_id', $filtered_projects->merge($depends)->pluck('id')) 184 | ->get(); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/resources/assets/sass/app.scss: -------------------------------------------------------------------------------- 1 | // Fonts 2 | @import url("https://fonts.googleapis.com/css?family=Arsenal:400"); 3 | 4 | // Variables 5 | @import "variables"; 6 | 7 | // Bootstrap 8 | @import "~bootstrap/scss/bootstrap"; 9 | 10 | // Bootstrap 11 | @import "~csspin/csspin"; 12 | 13 | body { 14 | color: $brand-darker; 15 | font-family: 'Arial'; 16 | font-weight: 500; 17 | } 18 | 19 | /* 20 | * Global add-ons 21 | */ 22 | 23 | .container-fluid { 24 | padding-top: 40px; 25 | } 26 | 27 | /* 28 | * Sidebar 29 | */ 30 | 31 | /* Hide for mobile, show later */ 32 | .sidebar { 33 | display: none; 34 | } 35 | 36 | /* 37 | * Tables 38 | */ 39 | 40 | .table-header { 41 | vertical-align: middle; 42 | margin-bottom: 15px; 43 | font-weight: 800; 44 | } 45 | 46 | .table-header .title { 47 | font-size: 1.8em; 48 | } 49 | 50 | .dim { 51 | filter: alpha(opacity=40); /* internet explorer */ 52 | -khtml-opacity: 0.4; /* khtml, old safari */ 53 | -moz-opacity: 0.4; /* mozilla, netscape */ 54 | opacity: 0.4; /* fx, safari, opera */ 55 | } 56 | 57 | .pale { 58 | filter: alpha(opacity=10); /* internet explorer */ 59 | -khtml-opacity: 0.1; /* khtml, old safari */ 60 | -moz-opacity: 0.1; /* mozilla, netscape */ 61 | opacity: 0.1; /* fx, safari, opera */ 62 | } 63 | 64 | th { 65 | font-size: 1.3em; 66 | } 67 | 68 | .table > tbody > tr > td { 69 | vertical-align: middle; 70 | } 71 | 72 | /* 73 | * Links 74 | */ 75 | a.file { 76 | color: $brand-danger-light; 77 | } 78 | 79 | a:hover.file { 80 | color: $brand-bluish; 81 | } 82 | 83 | /* 84 | * Projects 85 | */ 86 | .project-title { 87 | color: $brand-clear; 88 | text-transform: lowercase; 89 | font-size: 1.8em; 90 | margin-top: -7px; 91 | margin-bottom: 7px; 92 | font-weight: 800; 93 | } 94 | 95 | .search-project { 96 | color: #f8f9fa; 97 | background-color: #444; 98 | border: 1px solid #818182; 99 | } 100 | 101 | .search-project:focus { 102 | background-color: #444; 103 | border-color: #818182; 104 | color: #f8f9fa; 105 | } 106 | 107 | .card-projects { 108 | margin-bottom: 15px; 109 | background-color: $brand-darker; 110 | padding: 10px; 111 | } 112 | 113 | /** 114 | * Project list 115 | */ 116 | 117 | .list-group-item { 118 | cursor: pointer; 119 | border: 1px solid rgba(0, 0, 0, 0.125); 120 | } 121 | 122 | .list-group-item.active { 123 | background-color: $brand-pill; 124 | color: $brand-darkest; 125 | border: 1px solid rgba(0, 0, 0, 0.125); 126 | } 127 | 128 | list-group-item.active, .list-group-item.active:hover, .list-group-item.active:focus { 129 | background-color: $brand-pill-darker; 130 | } 131 | 132 | list-group-item, .list-group-item:hover, .list-group-item:focus { 133 | background-color: $brand-gray; 134 | } 135 | 136 | 137 | /* 138 | * Modals 139 | */ 140 | .modal-lg { 141 | max-width: 80% !important; 142 | max-height: 80% !important; 143 | } 144 | 145 | .modal-scroll { 146 | height: 650px; 147 | overflow-y: auto; 148 | margin-top: 15px; 149 | padding: 15px; 150 | } 151 | 152 | .terminal { 153 | background-color: $brand-darkest; 154 | } 155 | 156 | /* 157 | * Buttons 158 | */ 159 | .btn { 160 | padding: 3px 6px; 161 | cursor: pointer; 162 | } 163 | 164 | .btn-secondary { 165 | color: $brand-darkest !important; 166 | background-color: $brand-secondary !important; 167 | border-color: $brand-secondary-darker !important; 168 | } 169 | 170 | 171 | /* 172 | * Navbar 173 | */ 174 | .navbar { 175 | border-radius: 0px !important; 176 | margin-bottom: 0px !important; 177 | } 178 | 179 | .navbar-inverse { 180 | background-color: $brand-primary; 181 | border-color: $brand-primary; 182 | } 183 | 184 | .navbar-inverse .navbar-brand { 185 | color: $brand-clear; 186 | } 187 | 188 | /* 189 | * Toolbar 190 | */ 191 | .toolbar { 192 | background-color: $brand-gray; 193 | height: 60px; 194 | padding: 15px; 195 | vertical-align: middle; 196 | border: 1px; 197 | border-color: $brand-gray-darker; 198 | border-style: solid; 199 | } 200 | 201 | /* 202 | * Toolbar 203 | */ 204 | .btn-square { 205 | height: 30px; 206 | width: auto; 207 | padding: 5px; 208 | vertical-align: middle; 209 | padding-left: 10px; 210 | } 211 | 212 | /* 213 | * Search 214 | */ 215 | .search-group { 216 | padding: 5px; 217 | margin-top: -9px; 218 | width: 100%; 219 | } 220 | 221 | /* 222 | * Placeholder 223 | */ 224 | @mixin placeholder { 225 | ::-webkit-input-placeholder {@content} 226 | :-moz-placeholder {@content} 227 | ::-moz-placeholder {@content} 228 | :-ms-input-placeholder {@content} 229 | } 230 | 231 | @include placeholder { 232 | color: white; 233 | opacity: 0.3; 234 | } 235 | 236 | /* 237 | * State 238 | */ 239 | 240 | table > tbody > tr > td.state.state-failed { 241 | 242 | } 243 | 244 | .state { 245 | color: $brand-clear; 246 | } 247 | 248 | .state-failed { 249 | background-color: $brand-danger-light; 250 | } 251 | 252 | .state-running { 253 | background-color: $brand-info-light; 254 | } 255 | 256 | .state-ok { 257 | background-color: $brand-success-light; 258 | } 259 | 260 | .state-disabled { 261 | background-color: $brand-clear-light; 262 | color: $brand-darker; 263 | } 264 | 265 | .state-idle { 266 | background-color: $brand-darker-light; 267 | } 268 | 269 | .state-queued { 270 | background-color: $brand-gray-light; 271 | color: $brand-darker; 272 | } 273 | 274 | .badge { 275 | padding: 0.6em .8em .7em; 276 | } 277 | 278 | .project-state { 279 | font-size: 1.1em; 280 | } 281 | 282 | .project-state-passed { 283 | color: $brand-passed; 284 | } 285 | 286 | .project-state-failed { 287 | color: $brand-failed; 288 | } 289 | 290 | .screenshot { 291 | width: 98%; 292 | } 293 | 294 | .table-test-name { 295 | font-weight: 700 !important; 296 | color: $brand-darkest; 297 | } 298 | 299 | .table-test-path { 300 | font-size: 0.7rem; 301 | font-weight: 100 !important; 302 | color: $brand-gray-darker; 303 | } 304 | 305 | .table-link { 306 | cursor: pointer; 307 | } 308 | 309 | .html { 310 | color: $brand-clear-light; 311 | background-color: $brand-html; 312 | font-size: 1.2rem; 313 | font-weight: 500 !important; 314 | } 315 | 316 | .project-checkbox { 317 | margin-right: 5px; 318 | } 319 | 320 | .cursor-pointer { 321 | cursor: pointer; 322 | } 323 | 324 | h1, h2, h3 { 325 | font-weight: 800; 326 | } 327 | 328 | .app { 329 | margin-top: 40px; 330 | } 331 | -------------------------------------------------------------------------------- /src/package/Data/Repositories/Support/Projects.php: -------------------------------------------------------------------------------- 1 | $name], ['path' => $path, 'tests_path' => $tests_path]); 23 | } 24 | 25 | /** 26 | * Find project by id. 27 | * 28 | * @return \PragmaRX\Tddd\Package\Data\Models\Project|null 29 | */ 30 | public function findProjectById($id) 31 | { 32 | return Project::find($id); 33 | } 34 | 35 | /** 36 | * Delete unavailable projects. 37 | * 38 | * @param $projects 39 | */ 40 | public function deleteMissingProjects($projects) 41 | { 42 | foreach (Project::all() as $project) { 43 | if (!in_array($project->name, $projects)) { 44 | $project->delete(); 45 | } 46 | } 47 | } 48 | 49 | /** 50 | * Get all tests. 51 | * 52 | * @param null $project_id 53 | * 54 | * @return array 55 | */ 56 | public function getProjectTests($project_id = null) 57 | { 58 | $order = "(case 59 | when state = 'running' then 1 60 | when state = 'failed' then 2 61 | when state = 'queued' then 3 62 | when state = 'ok' then 4 63 | when state = 'idle' then 5 64 | end) asc, 65 | 66 | updated_at desc"; 67 | 68 | $query = Test::select('tddd_tests.*') 69 | ->join('tddd_suites', 'tddd_suites.id', '=', 'suite_id') 70 | ->orderByRaw($order); 71 | 72 | if ($project_id) { 73 | $query->where('project_id', $project_id); 74 | } 75 | 76 | return collect($query->get())->map(function ($test) { 77 | return $this->getTestInfo($test); 78 | }); 79 | } 80 | 81 | /** 82 | * Get all projects. 83 | * 84 | * @return \Illuminate\Database\Eloquent\Collection 85 | */ 86 | public function getProjectsAndCacheResult() 87 | { 88 | $projects = $this->getProjects(); 89 | 90 | app('tddd.cache')->put(Constants::CACHE_PROJECTS_KEY, sha1($projects->toJson())); 91 | 92 | return $projects; 93 | } 94 | 95 | /** 96 | * Get all projects. 97 | * 98 | * @return \Illuminate\Database\Eloquent\Collection 99 | */ 100 | public function getProjects() 101 | { 102 | return Project::all()->map(function ($item) { 103 | $item['tests'] = $this->getProjectTests($item->id); 104 | 105 | $item['state'] = $this->getProjectState(collect($item['tests'])); 106 | 107 | return $item; 108 | }); 109 | } 110 | 111 | /** 112 | * Get a SHA1 for all projects. 113 | * 114 | * @return bool 115 | */ 116 | public function projectSha1HasChanged() 117 | { 118 | $currentSha1 = sha1($projectsJson = $this->getProjects()->toJson()); 119 | 120 | $oldSha1 = app('tddd.cache')->get(Constants::CACHE_PROJECTS_KEY); 121 | 122 | if ($hasChanged = $currentSha1 != $oldSha1) { 123 | app('tddd.cache')->put(Constants::CACHE_PROJECTS_KEY, sha1($projectsJson)); 124 | } 125 | 126 | return $hasChanged; 127 | } 128 | 129 | /** 130 | * Get a SHA1 for all projects. 131 | * 132 | * @return \Illuminate\Database\Eloquent\Collection 133 | */ 134 | public function getProjectsSha1() 135 | { 136 | return sha1($this->getProjects()->toJson()); 137 | } 138 | 139 | /** 140 | * The the project state. 141 | * 142 | * @param \Illuminate\Support\Collection $tests 143 | * 144 | * @return string 145 | */ 146 | public function getProjectState($tests) 147 | { 148 | if ($tests->contains('state', 'running')) { 149 | return 'running'; 150 | } 151 | 152 | if ($tests->contains('state', 'queued')) { 153 | return 'queued'; 154 | } 155 | 156 | if ($tests->contains('state', 'failed')) { 157 | return 'failed'; 158 | } 159 | 160 | if ($tests->every('state', 'ok')) { 161 | return 'ok'; 162 | } 163 | 164 | return 'idle'; 165 | } 166 | 167 | /** 168 | * Enable tests. 169 | * 170 | * @param $enable 171 | * @param $project_id 172 | * 173 | * @return bool 174 | */ 175 | public function enableProjects($enable, $project_id) 176 | { 177 | $enable = $enable === 'true'; 178 | 179 | $projects = $project_id == 'all' 180 | ? Project::all() 181 | : Project::where('id', $project_id)->get(); 182 | 183 | foreach ($projects as $test) { 184 | $this->enableProject($enable, $test); 185 | } 186 | 187 | return $enable; 188 | } 189 | 190 | /** 191 | * Enable a test. 192 | * 193 | * @param $enable 194 | * @param \PragmaRX\Tddd\Package\Data\Models\Project $project 195 | */ 196 | protected function enableProject($enable, $project) 197 | { 198 | $project->timestamps = false; 199 | 200 | $project->enabled = $enable; 201 | 202 | $project->save(); 203 | } 204 | 205 | /** 206 | * Run all tests or projects tests. 207 | * 208 | * @param null $project_id 209 | */ 210 | public function runProjectTests($project_id = null) 211 | { 212 | $tests = $this->queryTests($project_id)->get(); 213 | 214 | foreach ($tests as $test) { 215 | $this->enableTest(true, $test); 216 | 217 | // Force test to the queue 218 | $this->runTest($test, true); 219 | } 220 | } 221 | 222 | /** 223 | * Run all tests or projects tests. 224 | * 225 | * @param null $project_id 226 | */ 227 | public function reset($project_id = null) 228 | { 229 | foreach ($this->queryTests($project_id)->get() as $test) { 230 | $this->resetTest($test); 231 | } 232 | } 233 | 234 | /** 235 | * Toggle the enabled state of all projects. 236 | */ 237 | public function toggleAll() 238 | { 239 | Project::all()->each(function ($project) { 240 | $this->enableProject(!$project->enabled, $project); 241 | }); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/resources/lang/en/validation.php: -------------------------------------------------------------------------------- 1 | 'The :attribute must be accepted.', 17 | 'active_url' => 'The :attribute is not a valid URL.', 18 | 'after' => 'The :attribute must be a date after :date.', 19 | 'after_or_equal' => 'The :attribute must be a date after or equal to :date.', 20 | 'alpha' => 'The :attribute may only contain letters.', 21 | 'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.', 22 | 'alpha_num' => 'The :attribute may only contain letters and numbers.', 23 | 'array' => 'The :attribute must be an array.', 24 | 'before' => 'The :attribute must be a date before :date.', 25 | 'before_or_equal' => 'The :attribute must be a date before or equal to :date.', 26 | 'between' => [ 27 | 'numeric' => 'The :attribute must be between :min and :max.', 28 | 'file' => 'The :attribute must be between :min and :max kilobytes.', 29 | 'string' => 'The :attribute must be between :min and :max characters.', 30 | 'array' => 'The :attribute must have between :min and :max items.', 31 | ], 32 | 'boolean' => 'The :attribute field must be true or false.', 33 | 'confirmed' => 'The :attribute confirmation does not match.', 34 | 'date' => 'The :attribute is not a valid date.', 35 | 'date_format' => 'The :attribute does not match the format :format.', 36 | 'different' => 'The :attribute and :other must be different.', 37 | 'digits' => 'The :attribute must be :digits digits.', 38 | 'digits_between' => 'The :attribute must be between :min and :max digits.', 39 | 'dimensions' => 'The :attribute has invalid image dimensions.', 40 | 'distinct' => 'The :attribute field has a duplicate value.', 41 | 'email' => 'The :attribute must be a valid email address.', 42 | 'exists' => 'The selected :attribute is invalid.', 43 | 'file' => 'The :attribute must be a file.', 44 | 'filled' => 'The :attribute field must have a value.', 45 | 'image' => 'The :attribute must be an image.', 46 | 'in' => 'The selected :attribute is invalid.', 47 | 'in_array' => 'The :attribute field does not exist in :other.', 48 | 'integer' => 'The :attribute must be an integer.', 49 | 'ip' => 'The :attribute must be a valid IP address.', 50 | 'ipv4' => 'The :attribute must be a valid IPv4 address.', 51 | 'ipv6' => 'The :attribute must be a valid IPv6 address.', 52 | 'json' => 'The :attribute must be a valid JSON string.', 53 | 'max' => [ 54 | 'numeric' => 'The :attribute may not be greater than :max.', 55 | 'file' => 'The :attribute may not be greater than :max kilobytes.', 56 | 'string' => 'The :attribute may not be greater than :max characters.', 57 | 'array' => 'The :attribute may not have more than :max items.', 58 | ], 59 | 'mimes' => 'The :attribute must be a file of type: :values.', 60 | 'mimetypes' => 'The :attribute must be a file of type: :values.', 61 | 'min' => [ 62 | 'numeric' => 'The :attribute must be at least :min.', 63 | 'file' => 'The :attribute must be at least :min kilobytes.', 64 | 'string' => 'The :attribute must be at least :min characters.', 65 | 'array' => 'The :attribute must have at least :min items.', 66 | ], 67 | 'not_in' => 'The selected :attribute is invalid.', 68 | 'numeric' => 'The :attribute must be a number.', 69 | 'present' => 'The :attribute field must be present.', 70 | 'regex' => 'The :attribute format is invalid.', 71 | 'required' => 'The :attribute field is required.', 72 | 'required_if' => 'The :attribute field is required when :other is :value.', 73 | 'required_unless' => 'The :attribute field is required unless :other is in :values.', 74 | 'required_with' => 'The :attribute field is required when :values is present.', 75 | 'required_with_all' => 'The :attribute field is required when :values is present.', 76 | 'required_without' => 'The :attribute field is required when :values is not present.', 77 | 'required_without_all' => 'The :attribute field is required when none of :values are present.', 78 | 'same' => 'The :attribute and :other must match.', 79 | 'size' => [ 80 | 'numeric' => 'The :attribute must be :size.', 81 | 'file' => 'The :attribute must be :size kilobytes.', 82 | 'string' => 'The :attribute must be :size characters.', 83 | 'array' => 'The :attribute must contain :size items.', 84 | ], 85 | 'string' => 'The :attribute must be a string.', 86 | 'timezone' => 'The :attribute must be a valid zone.', 87 | 'unique' => 'The :attribute has already been taken.', 88 | 'uploaded' => 'The :attribute failed to upload.', 89 | 'url' => 'The :attribute format is invalid.', 90 | 91 | /* 92 | |-------------------------------------------------------------------------- 93 | | Custom Validation Language Lines 94 | |-------------------------------------------------------------------------- 95 | | 96 | | Here you may specify custom validation messages for attributes using the 97 | | convention "attribute.rule" to name the lines. This makes it quick to 98 | | specify a specific custom language line for a given attribute rule. 99 | | 100 | */ 101 | 102 | 'custom' => [ 103 | 'attribute-name' => [ 104 | 'rule-name' => 'custom-message', 105 | ], 106 | ], 107 | 108 | /* 109 | |-------------------------------------------------------------------------- 110 | | Custom Validation Attributes 111 | |-------------------------------------------------------------------------- 112 | | 113 | | The following language lines are used to swap attribute place-holders 114 | | with something more reader friendly such as E-Mail Address instead 115 | | of "email". This simply helps us make messages a little cleaner. 116 | | 117 | */ 118 | 119 | 'attributes' => [], 120 | 121 | ]; 122 | -------------------------------------------------------------------------------- /src/package/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishConfiguration(); 40 | 41 | $this->loadConfig(); 42 | 43 | $this->loadMigrations(); 44 | 45 | $this->loadRoutes(); 46 | 47 | $this->loadViews(); 48 | } 49 | 50 | /** 51 | * Load config files to Laravel config. 52 | */ 53 | protected function loadConfig() 54 | { 55 | $this->config->loadConfig(); 56 | } 57 | 58 | /** 59 | * Configure migrations path. 60 | */ 61 | protected function loadMigrations() 62 | { 63 | $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); 64 | } 65 | 66 | /** 67 | * Configure views path. 68 | */ 69 | protected function loadViews() 70 | { 71 | $this->loadViewsFrom(__DIR__.'/../resources/views', 'pragmarx/tddd'); 72 | } 73 | 74 | /** 75 | * Configure config path. 76 | */ 77 | protected function publishConfiguration() 78 | { 79 | $this->publishes([ 80 | __DIR__.'/../config' => config_path(), 81 | ]); 82 | } 83 | 84 | /** 85 | * Register the service provider. 86 | * 87 | * @return void 88 | */ 89 | public function register() 90 | { 91 | if (!defined('TDDD_PATH')) { 92 | define('TDDD_PATH', realpath(__DIR__.'/../../')); 93 | } 94 | 95 | $this->registerResourceWatcher(); 96 | 97 | $this->registerService(); 98 | 99 | $this->registerWatcher(); 100 | 101 | $this->registerTester(); 102 | 103 | $this->registerConfig(); 104 | 105 | $this->registerCache(); 106 | 107 | $this->registerWatchCommand(); 108 | 109 | $this->registerTestCommand(); 110 | 111 | $this->registerNotifier(); 112 | 113 | $this->registerEventListeners(); 114 | } 115 | 116 | /** 117 | * Get the services provided by the provider. 118 | * 119 | * @return array 120 | */ 121 | public function provides() 122 | { 123 | return ['tddd']; 124 | } 125 | 126 | /** 127 | * Register event listeners. 128 | */ 129 | protected function registerEventListeners() 130 | { 131 | Event::listen(TestsFailed::class, Notify::class); 132 | 133 | Event::listen(UserNotifiedOfFailure::class, MarkAsNotified::class); 134 | } 135 | 136 | /** 137 | * Register the watch command. 138 | */ 139 | protected function registerNotifier() 140 | { 141 | $this->app->singleton('tddd.notifier', function () { 142 | return new Notifier(); 143 | }); 144 | } 145 | 146 | /** 147 | * Register the watch command. 148 | */ 149 | protected function registerWatchCommand() 150 | { 151 | $this->app->singleton('tddd.watch.command', function () { 152 | return new WatchCommand(); 153 | }); 154 | 155 | $this->commands('tddd.watch.command'); 156 | } 157 | 158 | /** 159 | * Register the test command. 160 | */ 161 | protected function registerTestCommand() 162 | { 163 | $this->app->singleton('tddd.test.command', function () { 164 | return new TestCommand(); 165 | }); 166 | 167 | $this->commands('tddd.test.command'); 168 | } 169 | 170 | /** 171 | * Register service service. 172 | */ 173 | protected function registerService() 174 | { 175 | $this->app->singleton('tddd', function () { 176 | return app('PragmaRX\Tddd\Package\Service'); 177 | }); 178 | } 179 | 180 | /** 181 | * Register service watcher. 182 | */ 183 | protected function registerWatcher() 184 | { 185 | $this->app->singleton('tddd.watcher', function () { 186 | return app('PragmaRX\Tddd\Package\Services\Watcher'); 187 | }); 188 | } 189 | 190 | /** 191 | * Register service tester. 192 | */ 193 | protected function registerTester() 194 | { 195 | $this->app->singleton('tddd.tester', function () { 196 | return app('PragmaRX\Tddd\Package\Services\Tester'); 197 | }); 198 | } 199 | 200 | /** 201 | * Register service tester. 202 | */ 203 | protected function registerCache() 204 | { 205 | $this->app->singleton('tddd.cache', function () { 206 | return new Cache(); 207 | }); 208 | } 209 | 210 | /** 211 | * Register service tester. 212 | */ 213 | protected function registerConfig() 214 | { 215 | $config = $this->config = app('PragmaRX\Tddd\Package\Services\Config'); 216 | 217 | $this->app->singleton('tddd.config', function () use ($config) { 218 | return $config; 219 | }); 220 | } 221 | 222 | /** 223 | * Register the resource watcher. 224 | */ 225 | protected function registerResourceWatcher() 226 | { 227 | $this->app->register('JasonLewis\ResourceWatcher\Integration\LaravelServiceProvider'); 228 | } 229 | 230 | /** 231 | * Register all routes. 232 | */ 233 | protected function loadRoutes() 234 | { 235 | Route::group([ 236 | 'prefix' => config('tddd.routes.prefixes.global'), 237 | 'namespace' => 'PragmaRX\Tddd\Package\Http\Controllers', 238 | 'middleware' => 'web', 239 | ], function () { 240 | $this->loadRoutesFrom(__DIR__.'/../routes/web.php'); 241 | }); 242 | } 243 | 244 | /** 245 | * Get the root directory for this ServiceProvider. 246 | * 247 | * @return string 248 | */ 249 | public function getRootDirectory() 250 | { 251 | return __DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'..'; 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/resources/assets/js/components/Projects.vue: -------------------------------------------------------------------------------- 1 | 98 | 99 | 171 | -------------------------------------------------------------------------------- /src/resources/assets/js/components/Log.vue: -------------------------------------------------------------------------------- 1 | 85 | 86 | 159 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TDDD - Test Driven Development Dashboard 2 | ### A Self-Hosted TDD Dashboard & Tests Watcher 3 | 4 | [![Latest Stable Version](https://img.shields.io/packagist/v/pragmarx/tddd.svg?style=flat-square)](https://packagist.org/packages/pragmarx/tddd) 5 | [![License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 6 | [![Downloads](https://img.shields.io/packagist/dt/pragmarx/tddd.svg?style=flat-square)](https://packagist.org/packages/pragmarx/tddd) 7 | [![Code Quality](https://img.shields.io/scrutinizer/g/antonioribeiro/tddd.svg?style=flat-square)](https://scrutinizer-tddd.com/g/antonioribeiro/tddd/?branch=master) 8 | [![Build](https://img.shields.io/scrutinizer/build/g/antonioribeiro/tddd.svg?style=flat-square)](https://scrutinizer-tddd.com/g/antonioribeiro/tddd/?branch=master) 9 | [![StyleCI](https://styleci.io/repos/27037779/shield)](https://styleci.io/repos/27037779) 10 | 11 | ## What is it? 12 | 13 | TDD Dashboard, is an app (built as a Laravel PHP package) to watch and run all your tests during development. It supports any test framework working on terminal, and comes with some testers (PHPUnit, phpspec, behat, Jest, AVA...) preconfigured, but you can easily add yours, just tell it where the executable is and it's done. It also shows the progress of your tests, let you run a single test or all of them, and open your favorite code editor (PHPStorm, VSCode, Sublime Text, etc.) going right to the failing line of your test. If your test framework generate screenshots, it is also able to show it in the log page, with all the reds and greens you are used to see in your terminal. 14 | 15 | It uses Laravel as motor, but supports (and has been tested with) many languages, frameworks and testing frameworks: 16 | 17 | * [PHPUnit](https://phpunit.de/) 18 | * [Laravel & Laravel Dusk](https://laravel.com/docs/5.5/dusk) 19 | * [Codeception](http://codeception.com/) 20 | * [phpspec](http://www.phpspec.net/en/stable/) 21 | * [Behat](http://behat.org/en/latest/) 22 | * [atoum](http://atoum.org/) 23 | * [Jest](https://facebook.github.io/jest/) 24 | * [AVA](https://github.com/avajs/ava) 25 | * [React](https://reactjs.org/) 26 | * [Ruby on Rails](http://guides.rubyonrails.org/testing.html) 27 | * [Nette Tester](https://tester.nette.org/) 28 | * [Symfony](https://symfony.com/doc/current/testing.html) 29 | 30 | ## Features 31 | 32 | * Project List: click a project link to see all its tests. 33 | * Open files directly in your source code editor (PHPStorm, Sublime Text...). 34 | * Error log with source code linked, go strait to the error line in your source code. 35 | * Enable/disable a test. Once disabled if the watcher catches a change in resources, that test will not fire. 36 | * Real time test state: "idle", "running", "queued", "ok" and "failed". 37 | * "Show" button, to display the error log of failed tests. 38 | * Highly configurable, watch anything and test everything! 39 | 40 | ### Videos 41 | 42 | - [Preview](https://www.youtube.com/watch?v=sO_aDf3xCgE) 43 | - [Installing](https://youtu.be/AgkKCLNiV8w) 44 | - [VueJS Preview](https://youtu.be/HAdfLYArk_A) 45 | - [Laravel Dusk Preview](https://youtu.be/ooF4oLD9U7Q) 46 | 47 | ### Screenshots 48 | 49 | #### Dashboard 50 | 51 | ![visits](https://raw.githubusercontent.com/antonioribeiro/tddd/master/docs/dashboard.png) 52 | 53 | #### Error Log 54 | ![visits](https://raw.githubusercontent.com/antonioribeiro/tddd/master/docs/errorlog1.png) 55 | 56 | ![visits](https://raw.githubusercontent.com/antonioribeiro/tddd/master/docs/errorlog2.png) 57 | 58 | ![visits](https://raw.githubusercontent.com/antonioribeiro/tddd/master/docs/errorlog3.png) 59 | 60 | ## Command Line Interface 61 | 62 | The Artisan commands **Watcher** and **Tester** are responsible for watching resources and firing tests, respectively: 63 | 64 | ### Watcher 65 | 66 | Keep track of your files and enqueue your tests every time a project or test file is changed. If a project file changes, it will enqueue all your tests, if a test file changes, it will enqueue only that particular test. This is how you run it: 67 | 68 | ``` bash 69 | php artisan tddd:watch 70 | ``` 71 | 72 | ### Tester 73 | 74 | Responsible for taking tests from the run queue, execute it and log the results. Tester will only execute enabled tests. This is how you run it: 75 | 76 | ``` bash 77 | php artisan tddd:test 78 | ``` 79 | 80 | ### Notifications 81 | 82 | It uses JoliNotif, so if it's not working on macOS, you can try installing terminal-notifier: 83 | 84 | ``` bash 85 | brew install terminal-notifier 86 | ``` 87 | 88 | ## Test Framework Compatibility 89 | 90 | This package was tested and is known to be compatible with 91 | 92 | * [Codeception](http://codeception.com/) 93 | * [PHPUnit](https://phpunit.de/) 94 | * [phpspec](http://www.phpspec.net/) 95 | * [behat](http://docs.behat.org/) 96 | * [atoum](https://github.com/atoum/atoum) 97 | * [Nette Tester](http://tester.nette.org/en/) 98 | 99 | ## Installing 100 | 101 | #### TL;DR 102 | 103 | ``` bash 104 | laravel new tddd 105 | cd tddd 106 | composer require pragmarx/tddd 107 | php artisan vendor:publish --provider="PragmaRX\Tddd\Package\ServiceProvider" 108 | valet link tddd 109 | # configure database on your .env 110 | php artisan migrate 111 | php artisan tddd:watch & php artisan tddd:work & 112 | open http://tddd.dev/tests-watcher/dashboard 113 | ``` 114 | 115 | ### Examples & Starter App 116 | 117 | For lots of examples, check [this starter app](https://github.com/antonioribeiro/tests-watcher-starter), which will also help you create an independent dashboard for your tests. 118 | 119 | ### The long version 120 | 121 | Require it with [Composer](http://getcomposer.org/): 122 | 123 | ``` bash 124 | composer require pragmarx/tddd 125 | ``` 126 | 127 | Create a database, configure on your Laravel app and migrate it 128 | 129 | ``` bash 130 | php artisan migrate 131 | ``` 132 | 133 | Publish Ci configuration: 134 | 135 | On Laravel 4.* 136 | 137 | Add the service provider to your app/config/app.php: 138 | 139 | ``` php 140 | 'PragmaRX\Tddd\Package\ServiceProvider', 141 | ``` 142 | 143 | ``` bash 144 | php artisan config:publish pragmarx/tddd 145 | ``` 146 | 147 | On Laravel 5.* 148 | 149 | ``` bash 150 | php artisan vendor:publish --provider="PragmaRX\Tddd\Package\ServiceProvider" 151 | ``` 152 | 153 | ## Example of projects 154 | 155 | ### Laravel Dusk 156 | 157 | ``` php 158 | 'project bar (dusk)' => [ 159 | 'path' => $basePath, 160 | 'watch_folders' => [ 161 | 'app', 162 | 'tests/Browser' 163 | ], 164 | 'exclude' => [ 165 | 'tests/Browser/console/', 166 | 'tests/Browser/screenshots/', 167 | ], 168 | 'depends' => [], 169 | 'tests_path' => 'tests', 170 | 'suites' => [ 171 | 'browser' => [ 172 | 'tester' => 'dusk', 173 | 'tests_path' => 'Browser', 174 | 'command_options' => '', 175 | 'file_mask' => '*Test.php', 176 | 'retries' => 0, 177 | ], 178 | ], 179 | ], 180 | ``` 181 | 182 | ## Troubleshooting 183 | 184 | #### Tests are running fine in terminal but failing in the dashboard? 185 | 186 | You have first to remember they are being executed in isolation, and, also, the environment is not exactly the same, so things like a cache and session may affect your results. 187 | 188 | ## Requirements 189 | 190 | - Laravel 4.1+ or 5 191 | - PHP 5.3.7+ 192 | 193 | ## Author 194 | 195 | [Antonio Carlos Ribeiro](http://twitter.com/iantonioribeiro) 196 | 197 | ## License 198 | 199 | Laravel Ci is licensed under the BSD 3-Clause License - see the `LICENSE` file for details 200 | 201 | ## Contributing 202 | 203 | Pull requests and issues are welcome. 204 | 205 | 206 | 207 | 208 | -------------------------------------------------------------------------------- /src/package/Data/Repositories/Support/Tests.php: -------------------------------------------------------------------------------- 1 | getRelativePathname()) 24 | ->where('suite_id', $suite->id) 25 | ->first(); 26 | 27 | return $exists; 28 | } 29 | 30 | /** 31 | * Create or update a test. 32 | * 33 | * @param \Symfony\Component\Finder\SplFileInfo $file 34 | * @param \PragmaRX\Tddd\Package\Data\Models\Suite $suite 35 | * 36 | * @return bool 37 | */ 38 | public function createOrUpdateTest($file, $suite) 39 | { 40 | $test = Test::where('path', $path = $this->normalizePath($file->getPath())) 41 | ->where('name', $name = trim($file->getFilename())) 42 | ->first(); 43 | 44 | if (is_null($test)) { 45 | $test = Test::create([ 46 | 'sha1' => sha1("$path/$name"), 47 | 'path' => $path, 48 | 'name' => $name, 49 | 'suite_id' => $suite->id, 50 | ]); 51 | } 52 | 53 | if ($test->wasRecentlyCreated && $this->findTestByFileAndSuite($file, $suite)) { 54 | $this->addTestToQueue($test); 55 | } 56 | 57 | return $test->wasRecentlyCreated; 58 | } 59 | 60 | /** 61 | * Sync all tests. 62 | * 63 | * @param $exclusions 64 | */ 65 | public function syncTests($exclusions, $showTests) 66 | { 67 | foreach ($this->getSuites() as $suite) { 68 | $this->syncSuiteTests($suite, $exclusions, $showTests); 69 | } 70 | } 71 | 72 | /** 73 | * Check if a file is a test file. 74 | * 75 | * @param $path 76 | * 77 | * @return \___PHPSTORM_HELPERS\static|bool|mixed 78 | */ 79 | public function isTestFile($path) 80 | { 81 | if (file_exists($path)) { 82 | foreach (Test::all() as $test) { 83 | if ($test->fullPath == $path) { 84 | return $test; 85 | } 86 | } 87 | } 88 | 89 | return false; 90 | } 91 | 92 | /** 93 | * Store the test result. 94 | * 95 | * @param $run 96 | * @param $test 97 | * @param $lines 98 | * @param $ok 99 | * @param $startedAt 100 | * @param $endedAt 101 | * 102 | * @return mixed 103 | */ 104 | public function storeTestResult($run, $test, $lines, $ok, $startedAt, $endedAt) 105 | { 106 | if (!$this->testExists($test)) { 107 | return false; 108 | } 109 | 110 | $run = $this->updateRun($run, $test, $lines, $ok, $startedAt, $endedAt); 111 | 112 | $test->state = $ok ? Constants::STATE_OK : Constants::STATE_FAILED; 113 | 114 | $test->last_run_id = $run->id; 115 | 116 | $test->save(); 117 | 118 | $this->removeTestFromQueue($test); 119 | 120 | return $ok; 121 | } 122 | 123 | /** 124 | * Mark a test as being running. 125 | * 126 | * @param $test 127 | * 128 | * @return mixed 129 | */ 130 | public function markTestAsRunning($test) 131 | { 132 | $test->state = Constants::STATE_RUNNING; 133 | 134 | $test->save(); 135 | 136 | return $this->createNewRunForTest($test); 137 | } 138 | 139 | /** 140 | * Find a test by name and suite. 141 | * 142 | * @param $suite 143 | * @param $file 144 | * 145 | * @return mixed 146 | */ 147 | protected function findTestByNameAndSuite($file, $suite) 148 | { 149 | return Test::where('name', $file->getRelativePathname())->where('suite_id', $suite->id)->first(); 150 | } 151 | 152 | /** 153 | * Enable tests. 154 | * 155 | * @param $enable 156 | * @param $project_id 157 | * @param null $test_id 158 | * 159 | * @return bool 160 | */ 161 | public function enableTests($enable, $project_id, $test_id) 162 | { 163 | $enable = is_bool($enable) ? $enable : ($enable === 'true'); 164 | 165 | $tests = $this->queryTests($project_id, $test_id == 'all' ? null : $test_id)->get(); 166 | 167 | foreach ($tests as $test) { 168 | $this->enableTest($enable, $test); 169 | } 170 | 171 | return $enable; 172 | } 173 | 174 | /** 175 | * Run a test. 176 | * 177 | * @param $test 178 | * @param bool $force 179 | */ 180 | public function runTest($test, $force = false) 181 | { 182 | if (!$test instanceof Test) { 183 | $test = Test::find($test); 184 | } 185 | 186 | $this->addTestToQueue($test, $force); 187 | } 188 | 189 | /** 190 | * Enable a test. 191 | * 192 | * @param $enable 193 | * @param \PragmaRX\Tddd\Package\Data\Models\Test $test 194 | */ 195 | protected function enableTest($enable, $test) 196 | { 197 | $test->timestamps = false; 198 | 199 | $test->enabled = $enable; 200 | 201 | $test->save(); 202 | 203 | if (!$enable) { 204 | $this->removeTestFromQueue($test); 205 | 206 | return; 207 | } 208 | 209 | if ($test->state !== Constants::STATE_OK) { 210 | $this->addTestToQueue($test); 211 | } 212 | } 213 | 214 | /** 215 | * Query tests. 216 | * 217 | * @param $test_id 218 | * 219 | * @return mixed 220 | */ 221 | protected function queryTests($projects, $test_id = null) 222 | { 223 | $projects = (array) $projects; 224 | 225 | $query = Test::select('tddd_tests.*') 226 | ->join('tddd_suites', 'tddd_suites.id', '=', 'tddd_tests.suite_id'); 227 | 228 | if ($projects && $projects != 'all') { 229 | $query->whereIn('tddd_suites.project_id', $projects); 230 | } 231 | 232 | if ($test_id && $test_id != 'all') { 233 | $query->where('tddd_tests.id', $test_id); 234 | } 235 | 236 | return $query; 237 | } 238 | 239 | /** 240 | * Mark tests as notified. 241 | * 242 | * @param $tests 243 | */ 244 | public function markTestsAsNotified($tests) 245 | { 246 | $tests->each(function ($test) { 247 | $test['run']->notified_at = Carbon::now(); 248 | 249 | $test['run']->save(); 250 | }); 251 | } 252 | 253 | /** 254 | * Check if the test exists. 255 | * 256 | * @param $test 257 | * 258 | * @return bool 259 | */ 260 | protected function testExists($test) 261 | { 262 | return !is_null(Test::find($test->id)); 263 | } 264 | 265 | /** 266 | * Update the run. 267 | * 268 | * @param $run 269 | * @param $test 270 | * @param $lines 271 | * @param $ok 272 | * @param $startedAt 273 | * @param $endedAt 274 | * 275 | * @return mixed 276 | */ 277 | private function updateRun($run, $test, $lines, $ok, $startedAt, $endedAt) 278 | { 279 | $run->test_id = $test->id; 280 | $run->was_ok = $ok; 281 | $run->log = $this->formatLog($lines, $test) ?: '(empty)'; 282 | $run->html = $this->getOutput($test, $test->suite->tester->output_folder, 283 | $test->suite->tester->output_html_fail_extension); 284 | $run->screenshots = $this->getScreenshots($test, $lines); 285 | $run->started_at = $startedAt; 286 | $run->ended_at = $endedAt; 287 | 288 | $run->save(); 289 | 290 | return $run; 291 | } 292 | } 293 | --------------------------------------------------------------------------------