diff --git a/CONTRIBUTING b/CONTRIBUTING new file mode 100644 index 0000000..0c04507 --- /dev/null +++ b/CONTRIBUTING @@ -0,0 +1,11 @@ +Each Contributor (“You”) represents that such You are legally entitled +to submit any Contributions in accordance with these terms and by +posting a Contribution, you represent that each of Your Contribution +is Your original creation. You are not expected to provide support +for Your Contributions, except to the extent You desire to provide +support. You may provide support for free, for a fee, or not at all. +Unless required by applicable law or agreed to in writing, You provide +Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS +OF ANY KIND, either express or implied, including, without limitation, +any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, +or FITNESS FOR A PARTICULAR PURPOSE.” diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 0000000..9b49b74 --- /dev/null +++ b/CONTRIBUTORS @@ -0,0 +1,7 @@ +Andrew Thomas +Angie Nguyen +Jason Boyle +John Welsh +Neha Saha +Scott Bishop +Stephen Marquis \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..11c641d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +#Dockerfile +FROM ruby:2.3 +LABEL maintainer="Scott Bishop - ScottBishop70@gmail.com" + +# Install tools & libs +RUN apt-get update +RUN apt-get install -y build-essential checkinstall libx11-dev libxext-dev zlib1g-dev libpng12-dev libjpeg-dev libfreetype6-dev libxml2-dev nodejs + +RUN apt-get install -y imagemagick libmagick++-dev libmagic-dev libmagickwand-dev vim libpq-dev && apt-get clean + +WORKDIR /app +COPY Gemfile* ./ +RUN gem install bundler +RUN bundle install + +COPY . /app + +# add encription key to decode secrets +ARG RAILS_MASTER_KEY + +RUN rake assets:precompile + +EXPOSE 3000 +CMD ["rails", "server", "-b", "0.0.0.0"] \ No newline at end of file diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..4dd586e --- /dev/null +++ b/Gemfile @@ -0,0 +1,102 @@ +source 'https://rubygems.org' + +ruby '2.3.1' + +# Bundle edge Rails instead: gem 'rails', github: 'rails/rails' +gem 'rails', '~> 5.1.4' +# Use Puma as the app server +gem 'puma', '~> 3.0' +# Use SCSS for stylesheets +gem 'sass-rails', '~> 5.0.7' +# Use Uglifier as compressor for JavaScript assets +gem 'uglifier', '>= 1.3.0' +# Use CoffeeScript for .coffee assets and views +gem 'coffee-rails', '~> 4.2' + +# Use jquery as the JavaScript library +gem 'jquery-rails' +# Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks +gem 'turbolinks', '~> 5.1.0' +# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder +gem 'jbuilder', '~> 2.5' + +gem 'bootswatch-rails' +gem 'bootstrap-sass', '~> 3.3.5' +gem 'devise-bootstrap-views' +gem 'sprockets-rails', require: 'sprockets/railtie' + +gem 'io-like' +# Use Gretel for breadcrumbs +gem 'gretel' + +# Issue monitoring and bug finder +gem 'bugsnag', '~> 6.6.3' + +gem 'rails_12factor' + +gem 'concurrent-ruby', '~> 1.0.2' + +# Use for uploading multipart images +gem 'multipart-post' + +gem 'json' + +# Use commontator for comments +gem 'commontator', '~> 5.1.0' +# bundle exec rake doc:rails generates the API under doc/api. +gem 'sdoc', '~> 1.0.0', group: :doc + +# Paperclip for attachments +gem 'paperclip', '~> 4.3' +# Imagemagick for perceptual diffs +gem 'rmagick' + +gem 'simple_token_authentication', '~> 1.15.1' + +# Devise for auth/accounts +gem 'devise' + +# LDAP for AD auth +gem 'net-ldap' + +# PG for postgresql +gem 'pg' + +# Use for translations +gem 'i18n', '0.7.0' + +# Ability for models have a tree structure (hierarchy) +gem 'ancestry' + +gem 'minitest-rails' +gem 'minitest-reporters', '~> 1.1.14' + +# Schedule cron tasks +gem 'whenever', require: false + +group :development, :test, :ci_test do + # Call 'byebug' anywhere in the code to stop execution and get a debugger console + gem 'byebug', platform: :mri + gem 'capybara' + gem 'chromedriver-helper' + gem 'factory_bot_rails' + gem 'minitest-rails-capybara' + gem 'rails-controller-testing' + gem 'selenium-webdriver' + # Mock requests for tests + gem 'webmock' +end + +group :development do + # Access an IRB console on exception pages or by using <%= console %> anywhere in the code. + gem 'listen', '~> 3.0.5' + gem 'web-console' + # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring + gem 'better_errors' + gem 'rails_real_favicon' + gem 'spring' + gem 'spring-watcher-listen', '~> 2.0.0' +end + +# Windows does not include zoneinfo files, so bundle the tzinfo-data gem +gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..46f609d --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,403 @@ +GEM + remote: https://rubygems.org/ + specs: + actioncable (5.1.4) + actionpack (= 5.1.4) + nio4r (~> 2.0) + websocket-driver (~> 0.6.1) + actionmailer (5.1.4) + actionpack (= 5.1.4) + actionview (= 5.1.4) + activejob (= 5.1.4) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 2.0) + actionpack (5.1.4) + actionview (= 5.1.4) + activesupport (= 5.1.4) + rack (~> 2.0) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + actionview (5.1.4) + activesupport (= 5.1.4) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.3) + activejob (5.1.4) + activesupport (= 5.1.4) + globalid (>= 0.3.6) + activemodel (5.1.4) + activesupport (= 5.1.4) + activerecord (5.1.4) + activemodel (= 5.1.4) + activesupport (= 5.1.4) + arel (~> 8.0) + activesupport (5.1.4) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (~> 0.7) + minitest (~> 5.1) + tzinfo (~> 1.1) + addressable (2.5.2) + public_suffix (>= 2.0.2, < 4.0) + ancestry (3.0.1) + activerecord (>= 3.2.0) + ansi (1.5.0) + archive-zip (0.11.0) + io-like (~> 0.3.0) + arel (8.0.0) + autoprefixer-rails (7.1.6) + execjs + bcrypt (3.1.11) + bcrypt (3.1.11-java) + bcrypt (3.1.11-x64-mingw32) + bcrypt (3.1.11-x86-mingw32) + better_errors (2.4.0) + coderay (>= 1.0.0) + erubi (>= 1.0.0) + rack (>= 0.9.0) + bindex (0.5.0) + bootstrap-sass (3.3.7) + autoprefixer-rails (>= 5.2.1) + sass (>= 3.3.4) + bootswatch-rails (3.3.5) + railties (>= 3.1) + bugsnag (6.6.3) + concurrent-ruby (~> 1.0) + builder (3.2.3) + byebug (9.1.0) + capybara (2.18.0) + addressable + mini_mime (>= 0.1.3) + nokogiri (>= 1.3.3) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + xpath (>= 2.0, < 4.0) + childprocess (0.9.0) + ffi (~> 1.0, >= 1.0.11) + chromedriver-helper (1.2.0) + archive-zip (~> 0.10) + nokogiri (~> 1.8) + chronic (0.10.2) + climate_control (0.2.0) + cocaine (0.5.8) + climate_control (>= 0.0.3, < 1.0) + coderay (1.1.2) + coffee-rails (4.2.2) + coffee-script (>= 2.2.0) + railties (>= 4.0.0) + coffee-script (2.4.1) + coffee-script-source + execjs + coffee-script-source (1.12.2) + commontator (5.1.0) + jquery-rails + rails (>= 5.0) + concurrent-ruby (1.0.5) + concurrent-ruby (1.0.5-java) + crack (0.4.3) + safe_yaml (~> 1.0.0) + crass (1.0.3) + devise (4.4.3) + bcrypt (~> 3.0) + orm_adapter (~> 0.1) + railties (>= 4.1.0, < 6.0) + responders + warden (~> 1.2.3) + devise-bootstrap-views (0.0.11) + domain_name (0.5.20170404) + unf (>= 0.0.5, < 1.0.0) + erubi (1.7.1) + execjs (2.7.0) + factory_bot (4.8.2) + activesupport (>= 3.0.0) + factory_bot_rails (4.8.2) + factory_bot (~> 4.8.2) + railties (>= 3.0.0) + ffi (1.9.23) + ffi (1.9.23-java) + ffi (1.9.23-x64-mingw32) + ffi (1.9.23-x86-mingw32) + globalid (0.4.1) + activesupport (>= 4.2.0) + gretel (3.0.9) + rails (>= 3.1.0) + hashdiff (0.3.7) + http-cookie (1.0.3) + domain_name (~> 0.5) + i18n (0.7.0) + io-like (0.3.0) + jbuilder (2.7.0) + activesupport (>= 4.2.0) + multi_json (>= 1.2) + jquery-rails (4.3.1) + rails-dom-testing (>= 1, < 3) + railties (>= 4.2.0) + thor (>= 0.14, < 2.0) + json (1.8.6) + json (1.8.6-java) + listen (3.0.8) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + loofah (2.2.2) + crass (~> 1.0.2) + nokogiri (>= 1.5.9) + mail (2.6.6) + mime-types (>= 1.16, < 4) + method_source (0.9.0) + mime-types (3.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2016.0521) + mimemagic (0.3.0) + mini_mime (1.0.0) + mini_portile2 (2.3.0) + minitest (5.11.3) + minitest-capybara (0.8.2) + capybara (~> 2.2) + minitest (~> 5.0) + rake + minitest-metadata (0.6.0) + minitest (>= 4.7, < 6.0) + minitest-rails (3.0.0) + minitest (~> 5.8) + railties (~> 5.0) + minitest-rails-capybara (3.0.1) + capybara (~> 2.7) + minitest-capybara (~> 0.8) + minitest-metadata (~> 0.6) + minitest-rails (~> 3.0) + minitest-reporters (1.1.18) + ansi + builder + minitest (>= 5.0) + ruby-progressbar + multi_json (1.12.2) + multipart-post (2.0.0) + net-ldap (0.16.0) + netrc (0.11.0) + nio4r (2.1.0) + nio4r (2.1.0-java) + nokogiri (1.8.2) + mini_portile2 (~> 2.3.0) + nokogiri (1.8.2-java) + nokogiri (1.8.2-x64-mingw32) + mini_portile2 (~> 2.3.0) + nokogiri (1.8.2-x86-mingw32) + mini_portile2 (~> 2.3.0) + orm_adapter (0.5.0) + paperclip (4.3.7) + activemodel (>= 3.2.0) + activesupport (>= 3.2.0) + cocaine (~> 0.5.5) + mime-types + mimemagic (= 0.3.0) + pg (0.21.0) + pg (0.21.0-x64-mingw32) + pg (0.21.0-x86-mingw32) + public_suffix (3.0.2) + puma (3.10.0) + puma (3.10.0-java) + rack (2.0.4) + rack-test (1.0.0) + rack (>= 1.0, < 3) + rails (5.1.4) + actioncable (= 5.1.4) + actionmailer (= 5.1.4) + actionpack (= 5.1.4) + actionview (= 5.1.4) + activejob (= 5.1.4) + activemodel (= 5.1.4) + activerecord (= 5.1.4) + activesupport (= 5.1.4) + bundler (>= 1.3.0) + railties (= 5.1.4) + sprockets-rails (>= 2.0.0) + rails-controller-testing (1.0.2) + actionpack (~> 5.x, >= 5.0.1) + actionview (~> 5.x, >= 5.0.1) + activesupport (~> 5.x) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) + rails-html-sanitizer (1.0.4) + loofah (~> 2.2, >= 2.2.2) + rails_12factor (0.0.3) + rails_serve_static_assets + rails_stdout_logging + rails_real_favicon (0.0.7) + json (>= 1.7, < 3) + rails (>= 3.1) + rest-client (~> 2.0) + rubyzip (~> 1) + rails_serve_static_assets (0.0.5) + rails_stdout_logging (0.0.5) + railties (5.1.4) + actionpack (= 5.1.4) + activesupport (= 5.1.4) + method_source + rake (>= 0.8.7) + thor (>= 0.18.1, < 2.0) + rake (12.3.1) + rb-fsevent (0.10.2) + rb-inotify (0.9.10) + ffi (>= 0.5.0, < 2) + rdoc (6.0.1) + responders (2.4.0) + actionpack (>= 4.2.0, < 5.3) + railties (>= 4.2.0, < 5.3) + rest-client (2.0.2) + http-cookie (>= 1.0.2, < 2.0) + mime-types (>= 1.16, < 4.0) + netrc (~> 0.8) + rest-client (2.0.2-x64-mingw32) + ffi (~> 1.9) + http-cookie (>= 1.0.2, < 2.0) + mime-types (>= 1.16, < 4.0) + netrc (~> 0.8) + rest-client (2.0.2-x86-mingw32) + ffi (~> 1.9) + http-cookie (>= 1.0.2, < 2.0) + mime-types (>= 1.16, < 4.0) + netrc (~> 0.8) + rest-client (2.0.2-x86-mswin32) + ffi (~> 1.9) + http-cookie (>= 1.0.2, < 2.0) + mime-types (>= 1.16, < 4.0) + netrc (~> 0.8) + rmagick (2.16.0) + ruby-progressbar (1.9.0) + rubyzip (1.2.1) + safe_yaml (1.0.4) + sass (3.5.2) + sass-listen (~> 4.0.0) + sass-listen (4.0.0) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + sass-rails (5.0.7) + railties (>= 4.0.0, < 6) + sass (~> 3.1) + sprockets (>= 2.8, < 4.0) + sprockets-rails (>= 2.0, < 4.0) + tilt (>= 1.1, < 3) + sdoc (1.0.0) + rdoc (>= 5.0) + selenium-webdriver (3.11.0) + childprocess (~> 0.5) + rubyzip (~> 1.2) + simple_token_authentication (1.15.1) + actionmailer (>= 3.2.6, < 6) + actionpack (>= 3.2.6, < 6) + devise (>= 3.2, < 6) + spring (2.0.2) + activesupport (>= 4.2) + spring-watcher-listen (2.0.1) + listen (>= 2.7, < 4.0) + spring (>= 1.2, < 3.0) + sprockets (3.7.1) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (3.2.1) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) + thor (0.20.0) + thread_safe (0.3.6) + thread_safe (0.3.6-java) + tilt (2.0.8) + turbolinks (5.1.0) + turbolinks-source (~> 5.1) + turbolinks-source (5.1.0) + tzinfo (1.2.5) + thread_safe (~> 0.1) + tzinfo-data (1.2017.3) + tzinfo (>= 1.0.0) + uglifier (3.2.0) + execjs (>= 0.3.0, < 3) + unf (0.1.4) + unf_ext + unf (0.1.4-java) + unf_ext (0.0.7.4) + unf_ext (0.0.7.4-x64-mingw32) + unf_ext (0.0.7.4-x86-mingw32) + warden (1.2.7) + rack (>= 1.0) + web-console (3.5.1) + actionview (>= 5.0) + activemodel (>= 5.0) + bindex (>= 0.4.0) + railties (>= 5.0) + webmock (3.2.1) + addressable (>= 2.3.6) + crack (>= 0.3.2) + hashdiff + websocket-driver (0.6.5) + websocket-extensions (>= 0.1.0) + websocket-driver (0.6.5-java) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.2) + whenever (0.9.7) + chronic (>= 0.6.3) + xpath (3.0.0) + nokogiri (~> 1.8) + +PLATFORMS + java + ruby + x64-mingw32 + x86-mingw32 + x86-mswin32 + +DEPENDENCIES + ancestry + better_errors + bootstrap-sass (~> 3.3.5) + bootswatch-rails + bugsnag (~> 6.6.3) + byebug + capybara + chromedriver-helper + coffee-rails (~> 4.2) + commontator (~> 5.1.0) + concurrent-ruby (~> 1.0.2) + devise + devise-bootstrap-views + factory_bot_rails + gretel + i18n (= 0.7.0) + io-like + jbuilder (~> 2.5) + jquery-rails + json + listen (~> 3.0.5) + minitest-rails + minitest-rails-capybara + minitest-reporters (~> 1.1.14) + multipart-post + net-ldap + paperclip (~> 4.3) + pg + puma (~> 3.0) + rails (~> 5.1.4) + rails-controller-testing + rails_12factor + rails_real_favicon + rmagick + sass-rails (~> 5.0.7) + sdoc (~> 1.0.0) + selenium-webdriver + simple_token_authentication (~> 1.15.1) + spring + spring-watcher-listen (~> 2.0.0) + sprockets-rails + turbolinks (~> 5.1.0) + tzinfo-data + uglifier (>= 1.3.0) + web-console + webmock + whenever + +RUBY VERSION + ruby 2.3.1p112 + +BUNDLED WITH + 1.16.1 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a6e513c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Workday + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7cdbea1 --- /dev/null +++ b/README.md @@ -0,0 +1,274 @@ +![vizzy-logo-3x](https://user-images.githubusercontent.com/1944329/38047018-ffe4638a-3275-11e8-8385-68c15493a908.png) + +Vizzy is a powerful Ruby on Rails web server that facilitates Visual Automation, a continuous integration testing strategy that aims to prevent visual regressions. It does this by performing pixel by pixel comparisons of screenshots captured during test runs. In doing so, it tests application data as well as application views. In order to harness the full power of Vizzy, there are two major prerequisites: + +1. A build pipeline that runs tests on pull requests. +2. A client testing framework that enables developers to easily write tests that can access the application's views, capture images of them, and save those images to a directory. + +The process Vizzy goes through is as follows: + +1. First a test suite is run against the master branch. This generates a set of images which becomes the baseline to test against. These "Base Images" are uploaded and managed by Vizzy. + +2. Then for every subsequent pull request changeset, the same test suite runs and generates a new set of images called "Test Images". MD5 hashes are created from the images so only the images that are different from their base image get uploaded. Vizzy goes through each matching test, which contains two images, one base image and one test image, and performs a pixel by pixel diff of the two images using a tool called [ImageMagick](https://rubygems.org/gems/rmagick/versions/2.15.4). A third image is generated which highlights the differences in red. + +3. After all diffs are calculated, Vizzy updates a Github status with the link to the visual overview. It displays: diffs, new tests, missing tests, test history, comments, and the ability to approve test images. The developer is then presented with the option to approve the differences if the changes were intentional, or to fix, commit, and rerun visual automation if the changes were unintentional or indicate a bug. Once the pull request is merged and subsequent master build finishes, the approved images replace the base images for those tests, and subsequent visual tests are compared against these new images. Large development teams can run multiple builds simultaneously because the base image set always matches the master branch. + +Vizzy is built with a plugin architecture. Plugins implement build hooks (build_created, build_committed, build_failed) to achieve a task. Slack, Jira, Bamboo, and Jenkins plugins are provided and configurable. + +[Vizzy Client-Server Diagram](https://user-images.githubusercontent.com/1944329/38047014-ff889078-3275-11e8-9b02-53fa9591e0f7.png) + +## Getting Started +### Prerequisites +Ruby 2.3.1 and Rails 5.1.4. See http://installrails.com/ for install instructions. + +#### Install ImageMagick and PostgreSQL +Install ImageMagick, a command line image processing tool (version 6 is required as the latest version causes bundle install to fail): + +``` +brew unlink imagemagick +brew install imagemagick@6 && brew link imagemagick@6 --force +``` + +Install the PostgreSQL database: + +`brew install postgres` + +PostgreSQL is a database server, so you'll need to start it up to run Vizzy. + +`brew services start postgresql` + +### Setup +Vizzy uses Rails 5.1 encrypted secrets for its configuration. From the Vizzy project directory, run these commands + +Generate the encrypted secrets file and key. + +``` +bin/rails secrets:setup +``` +Do not check in the encryption key into the repository, add it to the project with an environment variable called `RAILS_MASTER_KEY`. For more information you can go to this [blog post](https://www.engineyard.com/blog/encrypted-rails-secrets-on-rails-5.1). + +Add secrets to the yaml file by running the following command: *Note*: you may specify for your favorite editor. +``` +EDITOR=vi bin/rails secrets:edit +``` +If you see an error `Devise.secret_key was not set` you will have to add the sample secret_key to [devise.rb](/config/intitializers/devise.rb), run the setup/edit, then remove the Devise.secret_key. Devise now uses the project secret_key_base. + +Here are the possible secrets: + +```yaml +shared: + # Required Secrets + POSTGRES_USER: "postgres_database" + POSTGRES_PWD: "postgres_database_password" + GITHUB_AUTH_TOKEN: "auth_token_value" + ADMIN_EMAILS: "email1@gmail.com, email2@gmail.com" + # Optional Plugin Secrets + BAMBOO_USERNAME: "bamboo_username" + BAMBOO_PASSWORD: "bamboo_password" + JIRA_USERNAME: "jira_username" + JIRA_PASSWORD: "jira_password" + SLACK_WEBHOOK: "slack_webhook_value" + BUGSNAG_API_KEY: "bugsnag_api_key_value" + # Required To Run System Tests + GITHUB_ROOT_URL: "live_github_url" + GITHUB_REPO: "live_github_repo" + JIRA_PROJECT: "live_jira_project" + JIRA_BASE_URL: "live_jira_base_url" + BAMBOO_BASE_URL: "live_bamoo_base_url" + +development: + secret_key_base: "XXXXX1" + +test: + secret_key_base: "XXXXX2" + +ci_test: + secret_key_base: "XXXXX3" + +production: + secret_key_base: "XXXXX4" +``` +To generate new instances of `secret_key_base` run `rake secret` 4 times and copy/paste the values above. Upon saving this file, a file called `secrets.yml.enc` will be created. + +### Configure Authentication Type +To configure open [vizzy.yaml](config/vizzy.yaml) + +For local authentication with user registration, change the devise auth strategy to local. +```yaml +defaults: &default + devise: + auth_strategy: 'local' +``` + +For LDAP authentication/account lookup, configure your LDAP auth server. +```yaml +defaults: &default + devise: + auth_strategy: 'LDAP' + ldap_email_domain: '@domain.com' + ldap_host: 'host_ip_address' + ldap_port: 'host_port' + ldap_base: 'DC=domaininternal,DC=com' + ldap_email_internal_domain: '@domaininternal.com' + password_placeholder: "LDAP Password" +``` + +### Create and Migrate the Database +From the Vizzy project directory run + +`rake db:create db:migrate` + +### Create a Vizzy Project and Test User + +Vizzy projects allow you to run visual automation on multiple branches (ex. Master, Develop), each branch represented by a project. Each project has its own set of base images. There is typically a one to one mapping of a build plan to a Vizzy project. NOTE: Pull Requests opened against the master branch should be uploaded to the master Vizzy project so the correct set of base images are used to calculated the diffs. + +To create a test project for development, I recommend seeding the database. Vizzy uses [FactoryBot](https://github.com/thoughtbot/factory_bot) to add test data. Add your github information (required) as well as plugin settings (optional) to the project factory [project.rb](test/factories/projects.rb). + +Then run `rake db:reset` which will clear the database, run the migrations, and populate the database with the test data in [seeds.rb](db/seeds.rb)(sample user and project). You could also achieve this by running `rake db:seed` if you don't want to clear the database. + +You can also create a user account and navigate to http://0.0.0.0:3000/projects to create a new project once the server is running. + +### Create a Rails Run Configuration in your IDE ([RubyMine](https://www.jetbrains.com/ruby/)) +Run the server in development mode. Settings are straightforward. + +vizzy_development_run_configuration + +Navigate to http://0.0.0.0:3000 to see the running server. + +### Run Simulated Test Builds +There are 3 sets of sample images in /test-image-upload. There is an utility script [run_test_push.rb](test-image-upload/run_test_push.rb) to make running test builds easier. It only takes 2 parameters: `Test Case` and `Is Master`. Here are the 6 possible combinations which are easy to setup as Ruby Run Configurations. + +| Name | Test Case | Is Master | +| --- | --- | --- | +| Master 1 | 1 | 1 | +| Master 2 | 2 | 1 | +| Master 3 | 3 | 1 | +| Pull Request 1 | 1 | 2 | +| Pull Request 2 | 2 | 2 | +| Pull Request 3 | 3 | 2 | + +Here is a sample run config for Master 1: + +master_1_build_configuration + +## Deployment + +This server is docker ready and can be deployed with any tool you want. + +- [Dockerfile](Dockerfile): Builds dependencies, adds the source code, precompiles the assets, and exposes port 3000. + +We recommend a tool called [Kubernetes](https://kubernetes.io/docs/home/) (k8s), an opened sourced container cluster manager originally designed by Google, now owned by Cloud Native Computing. This tool aims to +provide a platform for automating deployment, scaling, and operations of application containers across clusters of hosts. + +Kubernetes scripts are provided in the [k8s](./k8s) folder. Here is an example for running the k8s deployment script. + +*NOTE*: some parameters will come from the Continuous Integration (CI) build system such as $GIT_COMMIT. + +```sh +API_SERVER=https://kubernetes-api-server.com +NAMESPACE=vizzy +REPLICA_PODS=5 +VIZZY_URI=vizzy.com +MEMORY="8Gi" +RAILS_ENV=production +DOCKER_REGISTRY=dockerhub.com +RUN_TESTS=false + +./deploy-vizzy.sh --api-server=$API_SERVER --bearer-token=$BEARER --rails-env=$RAILS_ENV --vizzy-version=$GIT_COMMIT --namespace=$NAMESPACE --vizzy-uri=$VIZZY_URI --replica-pods=$REPLICA_PODS --memory=$MEMORY --docker-registry=$DOCKER_REGISTRY --run-tests=$RUN_TESTS +``` + +### Our Current Deployment Setup + +1 Pod for Postgres database +5 Pods for the Vizzy server + +All pods are assigned an IP/Host in the data center and share: + +- A persistent volume (PVC - on the main host machine) which is mounted on every pod. All pods read and write images with this volume so as a user, no matter which of the 5 pods you connect to, images will +all be loaded correctly. +- A single Postgres database shared for all pods. + +## Add Upload Step to Continuous Integration Builds + +Vizzy contains the upload script in the public directory [upload_images_to_server.rb](test-image-upload/upload_images_to_server.rb). To obtain the upload script in your CI builds, you can download it from the running server with this shell script + +```sh +vizzy_endpoint="$1" +if [ -z "$vizzy_endpoint" ]; then + echo "No Vizzy endpoint given. Usage: './download_upload_script.sh '" + exit 1 +fi +echo "Downloading from $vizzy_endpoint/upload_images_to_server.rb" +curl -O "$vizzy_endpoint/upload_images_to_server.rb" +chmod a+x ./upload_images_to_server.rb +``` +This allows the upload script to be versioned with the server. + +There are 4 script options. Open is only used for creating Dev Builds and is not needed on CI builds. + +Usage: upload_images_to_server.rb command [options] + Available commands are: + create: Creates a new build with the server. Saves build information in json into a provided file path + upload: Upload provided images. Reads in build information from a file (as generated by the 'create' command) + open: Opens a visual build in a browser referenced in the build information file as generated by the 'create' command. + fail: Fail the build referenced in the build information file as generated by the 'create' command ) + +See 'upload_images_to_server.rb COMMAND --help' for more information on a specific command. + -h, --help Show this message + +### Examples using Bamboo CI +#### Create Vizzy Build +```sh + +VISUAL_HOST=https://vizzy.com +PLAN_KEY=${bamboo.planKey} +BUILD_NUMBER=${bamboo.buildNumber} +PLAN_TITLE=$PLAN_KEY-$BUILD_NUMBER +BUILD_URL="https://bamboo.com/browse/$PLAN_TITLE" +GIT_HASH=${bamboo.planRepository.1.revision} +VIZZY_USER_EMAIL=${bamboo.VIZZY_USER_EMAIL} +VIZZY_USER_TOKEN=${bamboo.VIZZY_USER_TOKEN_PASSWORD} + +ruby ./upload_images_to_server.rb create $VISUAL_HOST --title "$PLAN_TITLE" --project 1 --commit "$GIT_HASH" --file ./visual-build-info --url "$BUILD_URL" --user-email "$VIZZY_USER_EMAIL" --user-token "$VIZZY_USER_TOKEN" +``` + +#### Upload Images To Vizzy +```sh +#!/bin/sh +VISUAL_HOST=https://vizzy.com +TEST_IMAGE_DIR="../../android/application/visual-automation-test-images/visual-automation-device-images" +VIZZY_USER_EMAIL=${bamboo.VIZZY_USER_EMAIL} +VIZZY_USER_TOKEN=${bamboo.VIZZY_USER_TOKEN_PASSWORD} +ruby ./upload_images_to_server.rb upload $VISUAL_HOST --directory "$TEST_IMAGE_DIR" --file ./visual-build-info --user-email "$VIZZY_USER_EMAIL" --user-token "$VIZZY_USER_TOKEN" +``` + +#### Fail Vizzy Build +If your CI build fails at any time, you can notify Vizzy with a failure message +```sh +VISUAL_HOST=https://vizzy.com + VIZZY_USER_EMAIL=${bamboo.VIZZY_USER_EMAIL} + VIZZY_USER_TOKEN=${bamboo.VIZZY_USER_TOKEN_PASSWORD} + ruby ./upload_images_to_server.rb fail "$VISUAL_HOST" --message "Build Failed!" --file ./visual-build-info --user-email "$VIZZY_USER_EMAIL" --user-token "$VIZZY_USER_TOKEN" +``` + +### Non-Deterministic Challenges +Turn off Animations for test suites: taking screenshots of animations does not always capture the same image which will cause visual diffs. + +Mock Dynamic Data: taking screenshots that contain dates will change day to day and cause visual diffs. + +### Testing +Unit tests are run with the command + +```rake test``` + +In order to run System tests, fill out the system test encrypted secrets. System tests are run with the command + +```rails test:system``` + +## Contributing + +1. Fork the repo! +2. Create your feature branch: `git checkout -b my-new-feature` +3. Commit your changes: `git commit -am 'Add some feature'` +4. Push to the branch: `git push origin my-new-feature` +5. Submit a pull request :D diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..e85f913 --- /dev/null +++ b/Rakefile @@ -0,0 +1,6 @@ +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require_relative 'config/application' + +Rails.application.load_tasks diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js new file mode 100644 index 0000000..b16e53d --- /dev/null +++ b/app/assets/config/manifest.js @@ -0,0 +1,3 @@ +//= link_tree ../images +//= link_directory ../javascripts .js +//= link_directory ../stylesheets .css diff --git a/app/assets/images/.keep b/app/assets/images/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/assets/images/favicon/android-chrome-192x192.png b/app/assets/images/favicon/android-chrome-192x192.png new file mode 100644 index 0000000..ace1587 Binary files /dev/null and b/app/assets/images/favicon/android-chrome-192x192.png differ diff --git a/app/assets/images/favicon/android-chrome-512x512.png b/app/assets/images/favicon/android-chrome-512x512.png new file mode 100644 index 0000000..1d17804 Binary files /dev/null and b/app/assets/images/favicon/android-chrome-512x512.png differ diff --git a/app/assets/images/favicon/apple-touch-icon.png b/app/assets/images/favicon/apple-touch-icon.png new file mode 100644 index 0000000..ef85f1b Binary files /dev/null and b/app/assets/images/favicon/apple-touch-icon.png differ diff --git a/app/assets/images/favicon/browserconfig.xml.erb b/app/assets/images/favicon/browserconfig.xml.erb new file mode 100644 index 0000000..4983765 --- /dev/null +++ b/app/assets/images/favicon/browserconfig.xml.erb @@ -0,0 +1,9 @@ + + + + + + #da532c + + + diff --git a/app/assets/images/favicon/favicon-16x16.png b/app/assets/images/favicon/favicon-16x16.png new file mode 100644 index 0000000..d25dfcf Binary files /dev/null and b/app/assets/images/favicon/favicon-16x16.png differ diff --git a/app/assets/images/favicon/favicon-32x32.png b/app/assets/images/favicon/favicon-32x32.png new file mode 100644 index 0000000..ad8ba5f Binary files /dev/null and b/app/assets/images/favicon/favicon-32x32.png differ diff --git a/app/assets/images/favicon/favicon.ico b/app/assets/images/favicon/favicon.ico new file mode 100644 index 0000000..5afdb47 Binary files /dev/null and b/app/assets/images/favicon/favicon.ico differ diff --git a/app/assets/images/favicon/manifest.json.erb b/app/assets/images/favicon/manifest.json.erb new file mode 100644 index 0000000..cff1cc4 --- /dev/null +++ b/app/assets/images/favicon/manifest.json.erb @@ -0,0 +1,18 @@ +{ + "name": "", + "icons": [ + { + "src": "<%= asset_path 'favicon/android-chrome-192x192.png' %>", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "<%= asset_path 'favicon/android-chrome-512x512.png' %>", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} \ No newline at end of file diff --git a/app/assets/images/favicon/mstile-144x144.png b/app/assets/images/favicon/mstile-144x144.png new file mode 100644 index 0000000..0aaa435 Binary files /dev/null and b/app/assets/images/favicon/mstile-144x144.png differ diff --git a/app/assets/images/favicon/mstile-150x150.png b/app/assets/images/favicon/mstile-150x150.png new file mode 100644 index 0000000..6e11885 Binary files /dev/null and b/app/assets/images/favicon/mstile-150x150.png differ diff --git a/app/assets/images/favicon/mstile-310x150.png b/app/assets/images/favicon/mstile-310x150.png new file mode 100644 index 0000000..e0271f1 Binary files /dev/null and b/app/assets/images/favicon/mstile-310x150.png differ diff --git a/app/assets/images/favicon/mstile-310x310.png b/app/assets/images/favicon/mstile-310x310.png new file mode 100644 index 0000000..9c0fa1f Binary files /dev/null and b/app/assets/images/favicon/mstile-310x310.png differ diff --git a/app/assets/images/favicon/mstile-70x70.png b/app/assets/images/favicon/mstile-70x70.png new file mode 100644 index 0000000..ea3e2da Binary files /dev/null and b/app/assets/images/favicon/mstile-70x70.png differ diff --git a/app/assets/images/favicon/safari-pinned-tab.svg b/app/assets/images/favicon/safari-pinned-tab.svg new file mode 100644 index 0000000..916c38c --- /dev/null +++ b/app/assets/images/favicon/safari-pinned-tab.svg @@ -0,0 +1,30 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + diff --git a/app/assets/images/green_check.svg b/app/assets/images/green_check.svg new file mode 100644 index 0000000..bf27ea2 --- /dev/null +++ b/app/assets/images/green_check.svg @@ -0,0 +1,15 @@ + + + + green_check + Created with Sketch. + + + + + x + + + + + \ No newline at end of file diff --git a/app/assets/images/red_x.svg b/app/assets/images/red_x.svg new file mode 100644 index 0000000..1ebadcd --- /dev/null +++ b/app/assets/images/red_x.svg @@ -0,0 +1,13 @@ + + + + red_x + Created with Sketch. + + + + + + + + \ No newline at end of file diff --git a/app/assets/images/vizzy-logo.svg b/app/assets/images/vizzy-logo.svg new file mode 100644 index 0000000..703d025 --- /dev/null +++ b/app/assets/images/vizzy-logo.svg @@ -0,0 +1,24 @@ + + + + vizzy-icon + Created with Sketch. + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/assets/images/vizzy-text.svg b/app/assets/images/vizzy-text.svg new file mode 100644 index 0000000..9e2e4a6 --- /dev/null +++ b/app/assets/images/vizzy-text.svg @@ -0,0 +1,12 @@ + + + + vizzy-typelogo + Created with Sketch. + + + + + \ No newline at end of file diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js new file mode 100644 index 0000000..6ef234e --- /dev/null +++ b/app/assets/javascripts/application.js @@ -0,0 +1,17 @@ +// This is a manifest file that'll be compiled into application.js, which will include all the files +// listed below. +// +// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, +// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. +// +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// compiled file. +// +// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details +// about supported directives. +// +//= require jquery +//= require jquery_ujs +//= require turbolinks +//= require bootstrap-sprockets +//= require_tree . diff --git a/app/assets/javascripts/builds.js.coffee b/app/assets/javascripts/builds.js.coffee new file mode 100644 index 0000000..43d766d --- /dev/null +++ b/app/assets/javascripts/builds.js.coffee @@ -0,0 +1,4 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ + diff --git a/app/assets/javascripts/cable.js b/app/assets/javascripts/cable.js new file mode 100644 index 0000000..71ee1e6 --- /dev/null +++ b/app/assets/javascripts/cable.js @@ -0,0 +1,13 @@ +// Action Cable provides the framework to deal with WebSockets in Rails. +// You can generate new channels where WebSocket features live using the rails generate channel command. +// +//= require action_cable +//= require_self +//= require_tree ./channels + +(function() { + this.App || (this.App = {}); + + App.cable = ActionCable.createConsumer(); + +}).call(this); diff --git a/app/assets/javascripts/channels/.keep b/app/assets/javascripts/channels/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/assets/javascripts/diffs.js.coffee b/app/assets/javascripts/diffs.js.coffee new file mode 100644 index 0000000..05e0a37 --- /dev/null +++ b/app/assets/javascripts/diffs.js.coffee @@ -0,0 +1,8 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ + +$ -> + $("#toggle").click -> + console.log("togglin!") + $("#fullDiffs").toggle() \ No newline at end of file diff --git a/app/assets/javascripts/panels.js.coffee b/app/assets/javascripts/panels.js.coffee new file mode 100644 index 0000000..cdb39c5 --- /dev/null +++ b/app/assets/javascripts/panels.js.coffee @@ -0,0 +1,21 @@ +@togglePanel = (element) -> + button = $(element) + state = button.data("state") + spanClass0 = button.data("span-class-0") + spanClass1 = button.data("span-class-1") + contentId0 = button.data("content-id-0") + contentId1 = button.data("content-id-1") + glyphiconSpan = button.children('span:first') + if (!state? || state == "0") + glyphiconSpan.removeClass(spanClass0) + glyphiconSpan.addClass(spanClass1) + $(element).closest('.panel').find(contentId0).hide() + $(element).closest('.panel').find(contentId1).show() + button.data("state", "1") + else + glyphiconSpan.removeClass(spanClass1) + glyphiconSpan.addClass(spanClass0) + $(element).closest('.panel').find(contentId1).hide() + $(element).closest('.panel').find(contentId0).show() + button.data("state", "0") + diff --git a/app/assets/javascripts/projects.js.coffee b/app/assets/javascripts/projects.js.coffee new file mode 100644 index 0000000..24f83d1 --- /dev/null +++ b/app/assets/javascripts/projects.js.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/test_images.js.coffee b/app/assets/javascripts/test_images.js.coffee new file mode 100644 index 0000000..24f83d1 --- /dev/null +++ b/app/assets/javascripts/test_images.js.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/tests.js.coffee b/app/assets/javascripts/tests.js.coffee new file mode 100644 index 0000000..24f83d1 --- /dev/null +++ b/app/assets/javascripts/tests.js.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/users.coffee b/app/assets/javascripts/users.coffee new file mode 100644 index 0000000..24f83d1 --- /dev/null +++ b/app/assets/javascripts/users.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/stylesheets/_builds.scss b/app/assets/stylesheets/_builds.scss new file mode 100644 index 0000000..f9febca --- /dev/null +++ b/app/assets/stylesheets/_builds.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the Builds controller here. +// They will automatically be included in application.css.scss. +// You can use Sass (SCSS) here: http://sass-lang.com/ \ No newline at end of file diff --git a/app/assets/stylesheets/_diffs.scss b/app/assets/stylesheets/_diffs.scss new file mode 100644 index 0000000..e69de29 diff --git a/app/assets/stylesheets/application.css.scss b/app/assets/stylesheets/application.css.scss new file mode 100644 index 0000000..0a25d00 --- /dev/null +++ b/app/assets/stylesheets/application.css.scss @@ -0,0 +1,94 @@ +/* + * This is a manifest file that'll be compiled into application.css, which will include all the files + * listed below. + * + * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, + * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. + * + * You're free to add application-wide styles to this file and they'll appear at the bottom of the + * compiled file so the styles you add here take precedence over styles defined in any styles + * defined in the other CSS/SCSS files in this directory. It is generally better to create a new + * file per style scope. + * + *=require commontator/application + */ + +// Override colors +$brand-primary: #FF8B3D !default; +$brand-success: #8EA618 !default; +$brand-info: #ffab73 !default; +$brand-warning: #E86915 !default; +$brand-danger: #c70650 !default; + +// Import bootstrap-sprockets +@import "bootstrap-sprockets"; + +// Import paper variables +@import "bootswatch/paper/variables"; + +// Then bootstrap itself +@import "bootstrap"; + +// Bootstrap body padding for fixed navbar +body { padding-top: 60px; } + +// And finally bootswatch style itself +@import "bootswatch/paper/bootswatch"; + +.btn { + font-weight: 500; +} + +.build-header { + display: flex; + justify-content: space-between; +} + +.vizzy-build-bottom-spacing { + margin-bottom: 10px; +} + +.navbar-brand { + padding: 5px; +} + +.panel-body .table td { + word-wrap: break-word; +} + +.panel-body .table img { + max-width: 100%; +} + +.space-left { + margin-left: 10px; +} + +.space-right { + margin-right: 10px; +} + +.space-top { + margin-top: 10px; +} + +.space-bottom { + margin-top: 10px; +} + +.glyphicon-spacing { + margin-top: 4px; + margin-right: 4px; +} + +.vizzy-text-padding { + margin-top: 6px; +} + +.link-danger { + color: $brand-danger +} + +.link-success { + color: $brand-success +} diff --git a/app/assets/stylesheets/breadcrumbs.css.scss b/app/assets/stylesheets/breadcrumbs.css.scss new file mode 100644 index 0000000..dbb2ef7 --- /dev/null +++ b/app/assets/stylesheets/breadcrumbs.css.scss @@ -0,0 +1,3 @@ +.breadcrumb-current { + +} \ No newline at end of file diff --git a/app/assets/stylesheets/projects.css.scss b/app/assets/stylesheets/projects.css.scss new file mode 100644 index 0000000..a85f2ba --- /dev/null +++ b/app/assets/stylesheets/projects.css.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the Projects controller here. +// They will automatically be included in application.css.scss. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/assets/stylesheets/scaffolds.css.scss b/app/assets/stylesheets/scaffolds.css.scss new file mode 100644 index 0000000..6ec6a8f --- /dev/null +++ b/app/assets/stylesheets/scaffolds.css.scss @@ -0,0 +1,69 @@ +body { + background-color: #fff; + color: #333; + font-family: verdana, arial, helvetica, sans-serif; + font-size: 13px; + line-height: 18px; +} + +p, ol, ul, td { + font-family: verdana, arial, helvetica, sans-serif; + font-size: 13px; + line-height: 18px; +} + +pre { + background-color: #eee; + padding: 10px; + font-size: 11px; +} + +a { + color: #000; + &:visited { + color: #666; + } + &:hover { + color: #fff; + background-color: #000; + } +} + +div { + &.field, &.actions { + margin-bottom: 10px; + } +} + +#notice { + color: green; +} + +.field_with_errors { + padding: 2px; + background-color: red; + display: table; +} + +#error_explanation { + width: 450px; + border: 2px solid red; + padding: 7px; + padding-bottom: 0; + margin-bottom: 20px; + background-color: #f0f0f0; + h2 { + text-align: left; + font-weight: bold; + padding: 5px 5px 5px 15px; + font-size: 12px; + margin: -7px; + margin-bottom: 0px; + background-color: #c00; + color: #fff; + } + ul li { + font-size: 12px; + list-style: square; + } +} diff --git a/app/assets/stylesheets/test_images.css.scss b/app/assets/stylesheets/test_images.css.scss new file mode 100644 index 0000000..6066e82 --- /dev/null +++ b/app/assets/stylesheets/test_images.css.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the Images controller here. +// They will automatically be included in application.css.scss. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/assets/stylesheets/tests.css.scss b/app/assets/stylesheets/tests.css.scss new file mode 100644 index 0000000..22ea40f --- /dev/null +++ b/app/assets/stylesheets/tests.css.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the Tests controller here. +// They will automatically be included in application.css.scss. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/assets/stylesheets/users.scss b/app/assets/stylesheets/users.scss new file mode 100644 index 0000000..1efc835 --- /dev/null +++ b/app/assets/stylesheets/users.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the users controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb new file mode 100644 index 0000000..d672697 --- /dev/null +++ b/app/channels/application_cable/channel.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb new file mode 100644 index 0000000..0ff5442 --- /dev/null +++ b/app/channels/application_cable/connection.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Connection < ActionCable::Connection::Base + end +end diff --git a/app/config-helpers/system_test_config.rb b/app/config-helpers/system_test_config.rb new file mode 100644 index 0000000..700672f --- /dev/null +++ b/app/config-helpers/system_test_config.rb @@ -0,0 +1,26 @@ +class SystemTestConfig + include Singleton + + # Github + def self.github_root_url + Rails.application.secrets.GITHUB_ROOT_URL || 'https://github.com' + end + + def self.github_repo + Rails.application.secrets.GITHUB_REPO || 'mobile/android' + end + + # Jira + def self.jira_base_url + Rails.application.secrets.JIRA_BASE_URL || 'https://jira.com' + end + + def self.jira_project + Rails.application.secrets.JIRA_PROJECT || 'MOBILE' + end + + # Bamboo + def self.bamboo_base_url + Rails.application.secrets.BAMBOO_BASE_URL || 'https://bamboo.com' + end +end \ No newline at end of file diff --git a/app/config-helpers/vizzy_config.rb b/app/config-helpers/vizzy_config.rb new file mode 100644 index 0000000..d9d2e6e --- /dev/null +++ b/app/config-helpers/vizzy_config.rb @@ -0,0 +1,28 @@ +# Helper class for accessing vizzy.yaml config values preventing crashes if values don't exist +class VizzyConfig + include Singleton + + def initialize + @config = Rails.configuration.vizzy + end + + # Get config value or nil if it doesn't exist + # params: + # - Array of config values in order of lookup + def get_config_value(array) + value = @config + array.each do |key| + begin + value = value.fetch(key) + rescue + return nil + end + end + value + end + + # Convenience function to know if the server is configured to use ldap authentication + def is_ldap_auth + get_config_value(['devise', 'auth_strategy']) == 'LDAP' + end +end \ No newline at end of file diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 0000000..e0cd632 --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,22 @@ +class ApplicationController < ActionController::Base + # Prevent CSRF attacks by raising an exception. + protect_from_forgery with: :exception + before_action :validate_admin! + + # For APIs, use token based authentication + acts_as_token_authentication_handler_for User + + def validate_admin! + if admin_only && !current_user.admin? + redirect_to ('/404') + end + end + + def admin_only + false + end + + def json_request? + request.format.json? + end +end diff --git a/app/controllers/builds_controller.rb b/app/controllers/builds_controller.rb new file mode 100644 index 0000000..ee25f92 --- /dev/null +++ b/app/controllers/builds_controller.rb @@ -0,0 +1,263 @@ +require 'digest/md5' +class BuildsController < ApplicationController + before_action :set_build, only: [:show, :edit, :update, :destroy, :unapproved_diffs, :approved_diffs, :approve_all_images, :new_tests, :missing_tests, :add_md5s, :commit, :successful_tests, :fail] + skip_before_action :verify_authenticity_token, only: [:create, :add_md5s, :fail, :commit], if: :json_request? + + # GET /builds + # GET /builds.json + def index + @builds = Build.all + end + + # GET /builds/1 + # GET /builds/1.json + def show + end + + # GET /builds/new + def new + @build = Build.new + end + + # GET /builds/1/edit + def edit + end + + # POST /builds + # POST /builds.json + def create + begin + @build = Build.new(build_params) + @build.temporary = true + if @build.project.nil? + raise "Project #{build_params[:project_id]} does not exist" + end + update_vizzy_url_if_necessary(request.base_url) + @build.base_images = @build.project.calculate_base_images + @build.fetch_github_information + @build.update_dev_build_info + rescue StandardError => e + Bugsnag.notify(e) + project = Project.find(build_params[:project_id]) + hash = build_params[:commit_sha] + url_to_link = build_params[:url].blank? ? request.base_url : build_params[:url] + failure_message = e.message + if !@build.dev_build && !project.nil? + GithubService.run(project.github_root_url, project.github_repo) do |service| + service.send_project_status(project, url_to_link, hash, :failure, failure_message) + end + end + respond_to do |format| + format.html { redirect_to projects_path, notice: failure_message } + format.json { render json: { error: failure_message }, status: :internal_server_error } + end + return + end + + respond_to do |format| + if @build.save + @build.update_github_commit_status + results = PluginManager.instance.for_project(@build.project).run_build_created_hook(@build) + if results[:errors].blank? + format.html {redirect_to @build, notice: 'Build was successfully created.'} + format.json {render :show, status: :created, location: @build} + else + format.html {render :new} + format.json {render json: {error: results[:errors]}, status: :internal_server_error} + end + else + format.html { render :new } + format.json { render json: @build.errors, status: :unprocessable_entity } + end + end + end + + # POST /fail + # POST /fail.json + def fail + @build.fail_with_message(build_params[:failure_message]) + results = PluginManager.instance.for_project(@build.project).run_build_failed_hook(@build) + if results[:errors].blank? + format.html {redirect_to @build, notice: 'Build failed.'} + format.json {render :show, status: :ok, location: @build} + else + format.html {render :new} + format.json {render json: {error: results[:errors]}, status: :internal_server_error} + end + end + + def add_md5s + respond_to do |format| + if !@build.temporary? + format.html { redirect_to build_path(@build), alert: 'Cannot add md5s to a committed build' } + format.json { render json: { error: 'Cannot add md5s to a committed build' }, status: :bad_request } + elsif !@build.image_md5s.blank? + format.html { redirect_to build_path(@build), alert: 'Build already has md5s associated with it!' } + format.json { render json: { error: 'Build already has md5s associated with it!' }, status: :bad_request } + else + @build.image_md5s = JSON.parse(build_params[:image_md5s]) || {} + @build.full_list_of_image_md5s = @build.image_md5s + + # Update number of images in build to keep track + @build.num_of_images_in_build = @build.full_list_of_image_md5s.size + remove_matching_md5s_from_hash + + @build.image_md5s.each do |key, md5| + ensure_test_exists(key) + end + + if @build.save + format.html { redirect_to @build, notice: 'Md5s successfully added' } + format.json { render :show, status: :ok, location: @build } + else + format.html { render :new } + format.json { render json: @build.errors, status: :unprocessable_entity } + end + end + end + end + + def commit + if request.get? + respond_to do |format| + if @build.temporary + format.json { render json: { committed: false } } + else + format.json {render json: {committed: true, successful_test_count: @build.successful_tests.size, missing_tests_count: @build.missing_tests.size, unapproved_diffs_count: @build.unapproved_diffs.size, new_tests_count: @build.new_tests.size}} + end + end + elsif request.post? + respond_to do |format| + unless @build.temporary? + format.html { redirect_to build_path(@build), alert: 'Build already committed!' } + format.json { render json: { error: 'Build already committed!' }, status: :bad_request } + next + end + + unless @build.image_md5s.empty? + format.html { redirect_to build_path(@build), alert: "Not all images uploaded before commit! Missing #{@build.image_md5s.keys}" } + format.json { render json: { error: "Not all images uploaded before commit! Missing #{@build.image_md5s.keys}" }, status: :bad_request } + next + end + + BuildBackgroundCommitJob.perform_later(@build) + format.html { redirect_to @build, notice: 'Build commit in progress' } + format.json { render :show, status: :ok, location: @build } + end + end + end + + # Looks through image_md5s and base images and deletes the entry from the hash if there is a match. This leaves a list of images that need to be uploaded stored in + # image_md5s + def remove_matching_md5s_from_hash + preapproved_images = @build.preapproved_images_for_branch + @build.base_images.find_each(batch_size: 500) do |image| + @build.image_md5s.delete_if do |key, value| + # Delete the image if the md5, filename match and if there aren't preapprovals pending. If there are, the image needs to be + # uploaded because it could create diffs -- see handle_pull_request_preapproval_case in test_images_controller + preapprovals = preapproved_images[image.test_key] + if found_matching_md5_and_filename(key, value, image) && preapprovals.blank? + @build.successful_tests.push(image) + next true + end + next false + end + end + end + + # key and value of test image and base image is passed, returns true if they are a match + def found_matching_md5_and_filename(key, value, image) + !key.nil? && !value.nil? && !image.nil? && !image.md5.nil? && value.chomp == image.md5.chomp && key.chomp == image.test_key.chomp + end + + # PATCH/PUT /builds/1 + # PATCH/PUT /builds/1.json + def update + respond_to do |format| + if @build.update(build_params) + format.html { redirect_to @build, notice: 'Build was successfully updated.' } + format.json { render :show, status: :ok, location: @build } + else + format.html { render :edit } + format.json { render json: @build.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /builds/1 + # DELETE /builds/1.json + def destroy + @build.destroy + respond_to do |format| + format.html { redirect_to builds_url, notice: 'Build was successfully destroyed.' } + format.json { head :no_content } + end + end + + def approve_all_images + if @build.temporary? + redirect_to build_path(@build), alert: 'Cannot approve images until build has been committed' + else + @unapproved_diffs.each do |diff| + diff.approve(current_user) + diff.save + end + + @build.update_github_commit_status + + redirect_to build_path(@build) + end + end + + def unapproved_diffs + @diffs = @unapproved_diffs + render 'diffs/index' + end + + def approved_diffs + @diffs = @approved_diffs + render 'diffs/index' + end + + def new_tests + @test_images = @new_tests + render 'test_images/index' + end + + def missing_tests + @test_images = @missing_tests + render 'test_images/missing_tests' + end + + def successful_tests + @test_images = @successful_tests + render 'test_images/successful_tests' + end + + private + def update_vizzy_url_if_necessary(request_url) + if @build.project.vizzy_server_url != request_url + @build.project.vizzy_server_url = request_url + @build.project.save + end + end + + def ensure_test_exists(ancestry_key) + Test.create_or_find(@build.project_id, ancestry_key) + end + + # Use callbacks to share common setup or constraints between actions. + def set_build + @build = Build.find(params[:id]) + @unapproved_diffs = @build.unapproved_diffs + @approved_diffs = @build.approved_diffs + @new_tests = @build.new_tests + @missing_tests = @build.missing_tests + @successful_tests = @build.successful_tests + end + + # Never trust parameters from the scary internet, only allow the white list through. + def build_params + params.require(:build).permit(:url, :temporary, :title, :project_id, :pull_request_number, :commit_sha, :dev_build, :image_md5s, :failure_message) + end +end diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/controllers/diffs_controller.rb b/app/controllers/diffs_controller.rb new file mode 100644 index 0000000..e4a0e02 --- /dev/null +++ b/app/controllers/diffs_controller.rb @@ -0,0 +1,174 @@ +class DiffsController < ApplicationController + before_action :set_diff, only: [:show, :edit, :update, :destroy, :approve, :unapprove, :next, :multiple_diff_approval, :create_jira] + + # GET /diffs + # GET /diffs.json + def index + @diffs = Diff.all + end + + # GET /diffs/1 + # GET /diffs/1.json + def show + commontator_thread_show(@diff.new_image) + end + + # GET /diffs/new + def new + @diff = Diff.new + end + + # GET /diffs/1/edit + def edit + end + + # POST /diffs + # POST /diffs.json + def create + @diff = Diff.new(diff_params) + + respond_to do |format| + if @diff.save + format.html { redirect_to @diff, notice: 'Diff was successfully created.' } + format.json { render :show, status: :created, location: @diff } + else + format.html { render :new } + format.json { render json: @diff.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /diffs/1 + # PATCH/PUT /diffs/1.json + def update + respond_to do |format| + if @diff.update(diff_params) + format.html { redirect_to @diff, notice: 'Diff was successfully updated.' } + format.json { render :show, status: :ok, location: @diff } + else + format.html { render :edit } + format.json { render json: @diff.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /diffs/1 + # DELETE /diffs/1.json + def destroy + @diff.destroy + respond_to do |format| + format.html { redirect_to diffs_url, notice: 'Diff was successfully destroyed.' } + format.json { head :no_content } + end + end + + def create_jira + plugin_hash = params[:plugin_hash] + jira_base_url = plugin_hash[:jira_base_url][:value] + jira_project = plugin_hash[:jira_project][:value] + jira_component = plugin_hash[:jira_component][:value] + + jira_args = { + description: "#{@diff.build.project.vizzy_server_url}/diffs/#{@diff.id}", + project: jira_project, + component: jira_component, + title: "Visual Automation Test Issue: #{@diff.old_image.test.name}", + diff_id: @diff.id, + jira_base_url: jira_base_url + } + redirect_to new_jira_path(jira: jira_args) + end + + def approve + unless @diff.build.can_approve_images + redirect_to diff_path(@diff), alert: 'Cannot modify diffs until build has been committed' + return + end + + @diff.approve(current_user) + puts 'Redirecting to next diff' + go_to_next_diff_or_build_page + end + + def unapprove + unless @diff.build.can_approve_images + redirect_to diff_path(@diff), alert: 'Cannot modify diffs until build has been committed' + return + end + + @diff.unapprove + @diff.build.update_github_commit_status + puts 'Redirecting to build page' + redirect_to build_path(@diff.build) + end + + # Build Develop Build Only - Special case method for when multiple developers make changes on the same image, view with multiple diffs shows and they are allowed to + # approve either one of the old images + def multiple_diff_approval + if @diff.build.temporary? + redirect_to diff_path(@diff), alert: 'Cannot modify diffs until build has been committed' + return + end + + @diff.approve_old_image(current_user) + puts 'Redirecting to next diff' + go_to_next_diff_or_build_page + end + + # Redirect to next diff or to the build if no more diffs + def go_to_next_diff_or_build_page + @diff.build.update_github_commit_status + if @diff.build.unapproved_diffs.empty? + puts 'Redirecting to build overview' + redirect_to build_path(@diff.build) + return + end + + last_diff = @diff.build.unapproved_diffs.last + next_diff = nil + next_diff_id = @diff.id + + while next_diff.nil? && !@diff.build.unapproved_diffs.empty? + next_diff_id += 1 + + if next_diff_id > last_diff.id + # out of bounds, go back to the beginning + next_diff = @diff.build.unapproved_diffs.first + break + end + + begin + next_diff = @diff.build.diffs.find_by_id(next_diff_id) + rescue RecordNotFound => e + Bugsnag.notify(e) + puts e + next + end + + if !next_diff.nil? && (next_diff.approved || next_diff.old_image.test_key == @diff.old_image.test_key || !(next_diff.build.id == @diff.build.id)) + next_diff = nil + end + end + + redirect_to diff_path(next_diff) + end + + # Navigate to the next diff that needs approval + def next + puts 'Clicked next' + go_to_next_diff_or_build_page + # Navigate to the next diff that needs approval + end + + private + + # Use callbacks to share common setup or constraints between actions. + def set_diff + @diff = Diff.find(params[:id]) + end + + # Never trust parameters from the scary internet, only allow the white list through. + def diff_params + params.require(:diff).permit(:old_image_id, :new_image_id) + end +end diff --git a/app/controllers/jiras_controller.rb b/app/controllers/jiras_controller.rb new file mode 100644 index 0000000..716acb0 --- /dev/null +++ b/app/controllers/jiras_controller.rb @@ -0,0 +1,100 @@ +class JirasController < ApplicationController + before_action :set_jira, only: [:show, :edit, :update, :destroy] + + # GET /jiras + # GET /jiras.json + def index + @jiras = Jira.all + end + + # GET /jiras/1 + # GET /jiras/1.json + def show + end + + # GET /jiras/new + def new + @jira = Jira.new(jira_params) + end + + # GET /jiras/1/edit + def edit + end + + # POST /jiras + # POST /jiras.json + def create + @jira = Jira.new(jira_params) + + respond_to do |format| + if @jira.save + response = create_jira + if response.is_a?(Hash) && response.key?(:error) + format.html { render :new } + format.json { render json: {error: response[:error]}, status: :unprocessable_entity } + else + if @jira.jira_link + format.html { redirect_to @jira.jira_link } + format.json { render :show, status: :created, location: @jira } + else + format.html { redirect_to diff_path(@jira.diff_id), notice: response } + format.json { render :show, status: :created, location: @jira } + end + end + else + format.html { render :new } + format.json { render json: @jira.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /jiras/1 + # PATCH/PUT /jiras/1.json + def update + respond_to do |format| + if @jira.update(jira_params) + response = create_jira + if response.is_a?(Hash) && response.key?(:error) + format.html { render :edit } + format.json { render json: {error: response[:error]}, status: :unprocessable_entity } + else + if @jira.jira_link + format.html { redirect_to @jira.jira_link} + format.json { render :show, status: :ok, location: @jira } + else + format.html { redirect_to diff_path(@jira.diff_id), notice: response } + format.json { render :show, status: :ok, location: @jira } + end + end + else + format.html { render :edit } + format.json { render json: @jira.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /jiras/1 + # DELETE /jiras/1.json + def destroy + @jira.destroy + respond_to do |format| + format.html { redirect_to jiras_url, notice: 'Jira was successfully destroyed.' } + format.json { head :no_content } + end + end + + private + def create_jira + @jira.create_jira_request + end + + # Use callbacks to share common setup or constraints between actions. + def set_jira + @jira = Jira.find(params[:id]) + end + + # Never trust parameters from the scary internet, only allow the white list through. + def jira_params + params.require(:jira).permit(:title, :project, :component, :issue_type, :description, :jira_key, :jira_link, :priority, :diff_id, :jira_base_url) + end +end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb new file mode 100644 index 0000000..7039f6e --- /dev/null +++ b/app/controllers/projects_controller.rb @@ -0,0 +1,129 @@ +class ProjectsController < ApplicationController + before_action :set_project, only: [:show, :edit, :update, :destroy, :base_images, :remove_all_base_images, :clean_base_image_state, :cleanup_uncommitted_builds, :base_images_test_images] + + # GET /projects + # GET /projects.json + def index + @projects = Project.all + end + + # GET /projects/1 + # GET /projects/1.json + def show + end + + # GET /projects/new + def new + @project = Project.new + end + + # GET /projects/1/edit + def edit + end + + # POST /projects + # POST /projects.json + def create + @project = Project.new(project_params) + save_plugin_settings(params[:plugin_settings], params[:enabled_plugins]) + + respond_to do |format| + if @project.save + format.html { redirect_to @project, notice: 'Project was successfully created.' } + format.json { render :show, status: :created, location: @project } + else + format.html { render :new } + format.json { render json: @project.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /projects/1 + # PATCH/PUT /projects/1.json + def update + save_plugin_settings(params[:plugin_settings], params[:enabled_plugins]) + + respond_to do |format| + if @project.update(project_params) + format.html { redirect_to @project, notice: 'Project was successfully updated.' } + format.json { render :show, status: :ok, location: @project } + else + format.html { render :edit } + format.json { render json: @project.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /projects/1 + # DELETE /projects/1.json + def destroy + @project.destroy + respond_to do |format| + format.html { redirect_to projects_url, notice: 'Project was successfully destroyed.' } + format.json { head :no_content } + end + end + + def save_plugin_settings(settings, enabled_plugins) + return if settings.blank? + settings.each do |setting| + settings_hash = settings[setting] + settings_hash[:enabled] = if enabled_plugins.blank? + false + else + enabled_plugins[setting] == 'true' + end + @project.plugin_settings[setting] = settings_hash + end + end + + # Deletes any uncommitted builds older than 4 hours (to prevent deleting builds that are in process) + def cleanup_uncommitted_builds + @project.builds.where(temporary: true).where('updated_at < ?', 4.hours.ago).order('id DESC').destroy_all + redirect_back(fallback_location: project_path(@project)) + end + + # Removes all base images in the current project + def remove_all_base_images + @project.tests.each do |test| + test.test_images.each { |image| image.remove_image_from_base_images } + end + redirect_back(fallback_location: root_path) + end + + # Removes all base images that were not in the last branch build + def clean_base_image_state + @project.remove_base_images_not_uploaded_in_last_branch_build + redirect_back(fallback_location: root_path) + end + + def builds + Build.find_by(project: @project) + end + + def base_images + render 'projects/base_images' + end + + def base_images_test_images + render 'projects/base_images_test_images' + end + + private + + # Use callbacks to share common setup or constraints between actions. + def set_project + @project = Project.find(params[:id]) + end + + # Never trust parameters from the scary internet, only allow the white list through. + def project_params + params.require(:project).permit(:name, :description, :github_root_url, :github_repo, :github_status_context) + end + + protected + + def admin_only + ['remove_all_base_images_project_path', 'clean_base_image_state_project_path', 'edit', 'destroy'].include?(action_name) + end +end diff --git a/app/controllers/test_images_controller.rb b/app/controllers/test_images_controller.rb new file mode 100644 index 0000000..8cf59b1 --- /dev/null +++ b/app/controllers/test_images_controller.rb @@ -0,0 +1,84 @@ +include Magick + +class TestImagesController < ApplicationController + skip_before_action :verify_authenticity_token, only: [:create], if: :json_request? + + def new + @test_image = TestImage.new + end + + def index + @test_images = TestImage.all + end + + def show + @test_image = TestImage.find(params[:id]) + commontator_thread_show(@test_image) + end + + def create + creation_params = image_params.dup + creation_params.delete(:test_image_ancestry) + + @test_image = TestImage.new(creation_params) + + test_image_ancestry = image_params[:test_image_ancestry] + + respond_to do |format| + fail_validation = lambda do |msg| + format.html { redirect_to :test_images, notice: msg } + format.json { render json: { error: msg }, status: :bad_request } + @test_image.destroy + return + end + + if !@test_image.build + fail_validation.call('Invalid build specified') + else + test = Test.create_or_find(@test_image.build.project.id, test_image_ancestry) + @test_image.test = test + if @test_image.save + result = @test_image.validate_md5 + if result == :read_error + fail_validation.call('Could not calculate image md5') + elsif result == :not_in_build + fail_validation.call('Matching MD5 not found provided in build') + elsif result == :mismatched + fail_validation.call('MD5 does not match MD5 provided to build') + else + @test_image.build.remove_md5_for_image(@test_image) + format.html { redirect_to @test_image, notice: 'Image successfully created' } + format.json { render :show, status: :created, location: @test_image } + end + else + # Error could not save image + format.html { render :new } + format.json { render json: @test_image.errors, status: :unprocessable_entity } + end + end + end + end + + def destroy + @test_image = TestImage.find(params[:id]) + + old_image_diffs = Diff.where(old_image_id: @test_image.id) + old_image_diffs.each { |diff| diff.destroy } + + new_image_diffs = Diff.where(new_image_id: @test_image.id) + new_image_diffs.each { |diff| diff.destroy } + + + @test_image.destroy + + redirect_to test_images_path + end + + private + + # Use strong_parameters for attribute whitelisting + # Be sure to update your create() and update() controller methods. + def image_params + params.permit(:image, :build_id, :test_id, :approved, :test_image_ancestry) + end +end diff --git a/app/controllers/tests_controller.rb b/app/controllers/tests_controller.rb new file mode 100644 index 0000000..3f0cd27 --- /dev/null +++ b/app/controllers/tests_controller.rb @@ -0,0 +1,82 @@ +class TestsController < ApplicationController + before_action :set_test, only: [:show, :edit, :update, :destroy, :remove_base_images] + + # GET /tests + # GET /tests.json + def index + @tests = Test.all + end + + # GET /tests/1 + # GET /tests/1.json + def show + end + + # GET /tests/new + def new + @test = Test.new + end + + # GET /tests/1/edit + def edit + end + + # To remove the base image, just mark it as not approved. All base images have to be approved by definition + def remove_base_images + @test.test_images.each { |image| image.remove_image_from_base_images } + redirect_to @test, notice: 'Base Image was successfully removed.' + end + + # POST /tests + # POST /tests.json + def create + @test = Test.new(test_params) + + respond_to do |format| + if @test.save + format.html { redirect_to @test, notice: 'Test was successfully created.' } + format.json { render :show, status: :created, location: @test } + else + format.html { render :new } + format.json { render json: @test.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /tests/1 + # PATCH/PUT /tests/1.json + def update + @test.comment_user = current_user.display_name + respond_to do |format| + if @test.update(test_params) + format.html { redirect_back fallback_location: tests_path, notice: 'Test was successfully updated.' } + format.json { render :show, status: :ok, location: @test } + else + format.html { render :edit } + format.json { render json: @test.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /tests/1 + # DELETE /tests/1.json + def destroy + @test.destroy + respond_to do |format| + format.html { redirect_to tests_url, notice: 'Test was successfully destroyed.' } + format.json { head :no_content } + end + end + + private + + # Use callbacks to share common setup or constraints between actions. + def set_test + @test = Test.find(params[:id]) + end + + # Never trust parameters from the scary internet, only allow the white list through. + def test_params + params.require(:test).permit(:name, :description, :project_id, :test_suite_id, :jira, :comment, :pull_request_link, :comment_user, :ancestry, :parent_id) + end +end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb new file mode 100644 index 0000000..c9b5170 --- /dev/null +++ b/app/controllers/users_controller.rb @@ -0,0 +1,48 @@ +class UsersController < ApplicationController + before_action :set_user, only: [:show, :update, :destroy, :show_authentication_token, :revoke_authentication_token] + + def index + @users = User.all + end + + def show + end + + def update + if @user.update(user_params) + redirect_back fallback_location: root_path, notice: "Successfully updated #{@user.display_name}" + else + redirect_back fallback_location: root_path, notice: "Error while updating: #{@user.errors}" + end + end + + def destroy + display_name = @user.delete + @user.delete + redirect_to users_path, notice: "Successfully removed #{display_name}" + end + + def show_authentication_token + redirect_back fallback_location: root_path, notice: "Authentication Token: #{@user.authentication_token}" + end + + def revoke_authentication_token + @user.authentication_token = Devise.friendly_token + @user.save + show_authentication_token + end + + private + + def set_user + @user = User.find(params[:id]) + end + + def user_params + params.require(:user).permit(:admin) + end + + def admin_only + action_name == 'update' || action_name == 'destroy' + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 0000000..a47001b --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,30 @@ +module ApplicationHelper + def bootstrap_class_for flash_type + {success: "alert-success", error: "alert-danger", alert: "alert-warning", notice: "alert-info"}[flash_type.to_sym] || flash_type.to_s + end + + def flash_messages(opts = {}) + flash.each do |msg_type, message| + concat(content_tag(:div, message, class: "alert #{bootstrap_class_for(msg_type)} alert-dismissible", role: 'alert') do + concat(content_tag(:button, class: 'close', data: {dismiss: 'alert'}) do + concat content_tag(:span, '×'.html_safe, 'aria-hidden' => true) + concat content_tag(:span, 'Close', class: 'sr-only') + end) + concat message + end) + end + nil + end + + # Render tree structure of tests + # Params: + # - tests: tests arranged in tree structured hash -- tests.arrange + def render_nested_tests(tests) + content_tag(:ul) do + tests.map do |test, sub_tests| + span_tag = content_tag(:li, (test.name + render_nested_tests(sub_tests)).html_safe) + link_to(span_tag, test_path(test)) + end.join.html_safe + end + end +end diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb new file mode 100644 index 0000000..d621e99 --- /dev/null +++ b/app/helpers/builds_helper.rb @@ -0,0 +1,2 @@ +module BuildsHelper +end diff --git a/app/helpers/diffs_helper.rb b/app/helpers/diffs_helper.rb new file mode 100644 index 0000000..d5b1939 --- /dev/null +++ b/app/helpers/diffs_helper.rb @@ -0,0 +1,2 @@ +module DiffsHelper +end diff --git a/app/helpers/jiras_helper.rb b/app/helpers/jiras_helper.rb new file mode 100644 index 0000000..2cace64 --- /dev/null +++ b/app/helpers/jiras_helper.rb @@ -0,0 +1,2 @@ +module JirasHelper +end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb new file mode 100644 index 0000000..db5c5ce --- /dev/null +++ b/app/helpers/projects_helper.rb @@ -0,0 +1,2 @@ +module ProjectsHelper +end diff --git a/app/helpers/test_images_helper.rb b/app/helpers/test_images_helper.rb new file mode 100644 index 0000000..11185c7 --- /dev/null +++ b/app/helpers/test_images_helper.rb @@ -0,0 +1,2 @@ +module TestImagesHelper +end diff --git a/app/helpers/tests_helper.rb b/app/helpers/tests_helper.rb new file mode 100644 index 0000000..c54aa99 --- /dev/null +++ b/app/helpers/tests_helper.rb @@ -0,0 +1,2 @@ +module TestsHelper +end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb new file mode 100644 index 0000000..2310a24 --- /dev/null +++ b/app/helpers/users_helper.rb @@ -0,0 +1,2 @@ +module UsersHelper +end diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 0000000..a009ace --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,2 @@ +class ApplicationJob < ActiveJob::Base +end diff --git a/app/jobs/build_background_commit_job.rb b/app/jobs/build_background_commit_job.rb new file mode 100644 index 0000000..ee25e4b --- /dev/null +++ b/app/jobs/build_background_commit_job.rb @@ -0,0 +1,154 @@ +class BuildBackgroundCommitJob < ApplicationJob + queue_as :default + + rescue_from(StandardError) do |exception| + puts "Background job failed!" + puts "#{exception.message}" + puts "#{exception.backtrace.join("\n")}" + Bugsnag.notify(exception) + raise exception + end + + def perform(build) + @build = build + create_diffs_for_build + @build.temporary = false + @build.save + @build.update_github_commit_status + results = PluginManager.instance.for_project(@build.project).run_build_commited_hook(@build) + unless results[:errors].blank? + Bugsnag.notify(Exception.new("Build Commit Hook Plugin Failed: #{results[:errors]}")) + end + end + + private + def get_base_image_for_test_image(image) + # Find correct image to compare to + @build.base_images.find_each(batch_size: 500) do |base| + if image.test_key == base.test_key + return base + end + end + nil + end + + def create_diffs_for_build + if @build.is_branch_build + preapproved_images = @build.preapproved_images_for_branch + apply_singular_preapprovals preapproved_images + create_branch_diffs preapproved_images + else + previous_preapprovals = @build.previous_preapprovals_for_pull_request + create_pr_diffs(previous_preapprovals) + end + end + + def create_branch_diffs(preapproved_images) + @build.test_images.find_each(batch_size: 500) do |image| + base = get_base_image_for_test_image(image) + images = preapproved_images[image.test_key] + if base.nil? + handle_new_image_added image + elsif !images.blank? # Single preapprovals have already been removed + images.each do |preapproved| + create_diff preapproved, image, true # Force diffs to show even if they're the same, for context of multiple preapprovals + end + else + create_diff base, image, false + end + end + end + + def create_pr_diffs(previous_preapprovals) + @build.test_images.find_each(batch_size: 500) do |image| + base = get_base_image_for_test_image(image) + if base.nil? + handle_new_image_added image + next + else + diff = create_diff base, image, false + previous = previous_preapprovals[image.test_key] + if previous and not diff.nil? # See if the previous matches the current and is being compared to the same base. If it is, approve the diff + previous_diff = previous.build.diffs.where(new_image: previous).last + if !previous_diff.nil? and previous_diff.old_image == diff.old_image + _, score = compare_images(previous, image) + if score == 0 + previous_approval_user = previous_diff.approved_by + diff.approve(previous_approval_user) + end + end + end + end + end + # Now clear previous preapprovals, since the new build supercedes it + previous_preapprovals.each do |_, image| + image.clear_preapproval_information(false) + image.save + end + end + + # Automatically approves the image as this is a new test + def handle_new_image_added(image) + return if @build.dev_build + image.approved = true + image.user_approved_this_build = true + image.image_created_this_build = true + image.save + end + + def apply_singular_preapprovals(preapproved_images) + preapproval_applied = false + preapproved_images.delete_if do |key, images| + if images.size == 1 + image = images.first + image.approved = true + image.clear_preapproval_information(true) + image.save + preapproval_applied = true + true + else + false + end + end + # Recalculate base images, since the preapprovals happened + if preapproval_applied + @build.base_images = @build.project.calculate_base_images + @build.save + end + end + + def create_diff(old_image, new_image, force) + return nil unless old_image && new_image + difference_image, score = compare_images(old_image, new_image) + if score == 0 && !force + @build.successful_tests.push(new_image) + return nil + else + # Be sure to save the image and all of its changes to the database + new_image.save + end + differences_file = differences_file(difference_image) + diff = Diff.new(:old_image => old_image, :new_image => new_image, :differences => differences_file, :build => @build) + diff.save + diff + end + + # It Creates two imagelist and compare it using Absolute error metrics. + # It returns an array of two elements, first one is the difference image and second one is the score + def compare_images(baseline_image, candidate_image) + baseline_image = ImageList.new(baseline_image.image.path).first + candidate_image = ImageList.new(candidate_image.image.path).first + baseline_image.fuzz = '0%' + baseline_image.compare_channel(candidate_image, AbsoluteErrorMetric) + end + + # It creates a blob of the difference image. It then converts the blob into a String IO, which acts like a file + def differences_file(difference_image) + image_blob = difference_image.to_blob { |image_info| image_info.format = 'PNG' } + differences = StringIO.new(image_blob) + differences.class.class_eval { attr_accessor :original_filename, :content_type } + differences.original_filename = 'differences.png' + differences.content_type = 'image/png' + differences + end +end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 0000000..286b223 --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +class ApplicationMailer < ActionMailer::Base + default from: 'from@example.com' + layout 'mailer' +end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 0000000..10a4cba --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end diff --git a/app/models/build.rb b/app/models/build.rb new file mode 100644 index 0000000..79f3e94 --- /dev/null +++ b/app/models/build.rb @@ -0,0 +1,182 @@ +require 'net/http' +require 'net/https' +class Build < ActiveRecord::Base + serialize :image_md5s + serialize :full_list_of_image_md5s + serialize :associated_commit_shas + has_and_belongs_to_many :base_images, class_name: 'TestImage', join_table: 'builds_base_images' + has_and_belongs_to_many :successful_tests, class_name: 'TestImage', join_table: 'builds_successful_tests' + has_many :test_images, dependent: :destroy + has_many :diffs, dependent: :destroy + belongs_to :project + acts_as_commontable + + def new_tests + test_images.where(build_id: id).where(image_created_this_build: true) + end + + def missing_tests + get_base_images_not_uploaded + end + + def approved_diffs + diffs.where(build_id: id).where(approved: true) + end + + def unapproved_diffs + diffs.where(build_id: id).where(approved: false) + end + + def vizzy_build_url + "#{self.project.vizzy_server_url}/builds/#{id}" + end + + def get_base_images_not_uploaded + return [] if self.temporary? || self.full_list_of_image_md5s.nil? + self.base_images.where.not(test_key: self.full_list_of_image_md5s.keys) + end + + def remove_md5_for_image(image) + self.with_lock do # Since it's just a serialized hash, we have to take a lock to avoid overwriting the same data + self.image_md5s.except!(image.test_key) # Remove from the upload list, to prevent duplicates + self.save + end + end + + def fail_with_message(message) + self.failure_message = message + self.temporary = false + self.save + self.update_github_commit_status + end + + # Returns a hash with the message and status (one of :pending, :success, :failure or :forced_failure) + def current_state + if !self.failure_message.nil? + { message: self.failure_message, status: :forced_failure } + elsif temporary + { message: 'Running Visual Tests', status: :pending } + else + unapproved_count = self.unapproved_diffs.count + approved_count = self.approved_diffs.count + new_count = self.new_tests.count + message_components = [] + message_components.push("#{new_count} new tests") if new_count > 0 + + if unapproved_count > 0 + message_components.push("#{unapproved_count} unapproved diffs") + end + + if approved_count > 0 + message_components.push("#{approved_count} approved diffs") + end + + message = message_components.count > 0 ? message_components.join(', ') : '0 diffs' + status = unapproved_count > 0 ? :failure : :success + { message: message, status: status } + end + end + + def update_github_commit_status + return if self.dev_build + + GithubService.run(self.project.github_root_url, self.project.github_repo) do |service| + state = current_state + message = state[:message] + message += ' - Approval Needed' if state[:status] == :failure + github_status = state[:status] == :forced_failure ? :failure : state[:status] + service.send_status(self, github_status, message) + end + end + + # Queries github for metadata about the build, and stores it in the build object + def fetch_github_information + self.associated_commit_shas = [] + self.branch_name = nil + self.username = nil + + return if self.dev_build + + GithubService.run(self.project.github_root_url, self.project.github_repo) do |service| + if self.is_branch_build + self.associated_commit_shas = service.github_commits(10, self.commit_sha) + self.branch_name = nil + self.username = nil + else + self.associated_commit_shas = [] + info = service.user_and_branch_for_pull_request(self.pull_request_number) + self.username = info[:user] + self.branch_name = info[:branch] + end + end + end + + # Git shas are stored with an image when it is approved in a pull request. This function finds the list all shas associated with a build and returns all the preapproved images + # This is the case when two developers made changes regarding the same test image, and both made the same build after being merged into develop + # + # @return preapproved_images -- Hash where the key is the test key, and the value is the array of preapproved images for that test + def preapproved_images_for_branch + preapproved_images = {} + if self.is_branch_build + self.associated_commit_shas.each do |sha| + TestImage.where(image_pull_request_sha: sha, approved: false).select { |image| image.build.project.id == self.project.id }.each do |image| + if preapproved_images.key?(image.test_key) + if unique_preapproval_md5(preapproved_images[image.test_key], image) + preapproved_images[image.test_key].push(image) + else + image.clear_preapproval_information(false) + image.save + end + else + preapproved_images[image.test_key] = [image] + end + end + end + end + preapproved_images + end + + def unique_preapproval_md5(preapproved_images, image) + preapproved_images.each do |preapproved_image| + return false if preapproved_image.md5 == image.md5 + end + true + end + + # Git shas and PR numbers are stored with an image when it is approved in a pull request. This function searches previous builds for matching PR number and returns the last image that got pre-approved + # + # @return previous_preapproved_images -- Hash where the key is the test key, and the value is the previously preapproved image for that test + def previous_preapprovals_for_pull_request + previous_preapproved_images = {} + self.project.pull_requests(self.pull_request_number).each do |build| + next if build == self + + build.test_images.where(image_pull_request_number: self.pull_request_number).where.not(image_pull_request_sha: nil).each do |image| + previous_preapproved_images[image.test_key] = image + end + end + previous_preapproved_images + end + + # Pull Request Number is a string + # @return true if pull request, false if not a pull request + def is_branch_build + self.pull_request_number == '-1' + end + + def formatted_created_at_time + self.created_at.strftime('%b %d, %Y %I:%M:%S %P ') + end + + def update_dev_build_info + return unless self.dev_build + self.pull_request_number = nil + self.branch_name = nil + self.commit_sha = nil + self.title = 'Dev Build' + end + + def can_approve_images + !self.temporary? && !self.dev_build? + end +end diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/models/diff.rb b/app/models/diff.rb new file mode 100644 index 0000000..6dc1582 --- /dev/null +++ b/app/models/diff.rb @@ -0,0 +1,84 @@ +class Diff < ActiveRecord::Base + belongs_to :old_image, :class_name => 'TestImage' + belongs_to :new_image, :class_name => 'TestImage' + belongs_to :build, :class_name => 'Build' + belongs_to :approved_by, :class_name => 'User' + has_many :jiras, dependent: :destroy + has_attached_file :differences, path: ':rails_root/public/visual_images/:class/:attachment/:id_partition/:style/:filename', url: '/visual_images/:class/:attachment/:id_partition/:style/:filename', styles: {thumbnail: '125x', small: '300x'} + validates_attachment :differences, content_type: {content_type: ["image/jpg", "image/jpeg", "image/png", "image/gif"]} + + def approve(current_user) + if self.build.pull_request_number != '-1' + # This is the preapproved pull request case, don't approve the image, just store the commit_sha with the + # associated new image + # Add sha and pull request number to the image + self.new_image.preapprove(self.build.pull_request_number, self.build.commit_sha) + else + # This should rarely happen, but in the case of a diff in the branch build plan, allow a user to approve the + # image immediately + self.new_image.approved = true + cleanup_related_diffs current_user, true + end + + # Mark diff that an action has been taken, so it is listed in the "Images approved with this build" section + self.new_image.user_approved_this_build = true + self.new_image.save + self.approved_by = current_user + self.approved = true + self.save + end + + # Unapprove a given diff, used for if a mistake was made. Only available if the image had previously been approved + def unapprove + if self.build.pull_request_number != '-1' + # Clear sha and pull request number + self.new_image.clear_preapproval_information(false) + else + self.new_image.approved = false + end + + # Clear fields that mark diff that an action has been taken, so it is listed in the "Diffs waiting for approval" section + self.new_image.user_approved_this_build = false + self.new_image.save + self.approved_by = nil + self.approved = false + self.save + end + + # After `diff` has been approved, make changes to all the other diffs with the same test. This is for the + # multiple preapproval case + def cleanup_related_diffs(current_user, include_current) + self.build.diffs.each do |other_diff| + if (other_diff != self || include_current) && other_diff.old_image.test_key == self.old_image.test_key + # Remove preapproval metadata + other_diff.old_image.clear_preapproval_information(false) + other_diff.old_image.save + + # Mark diff approved, so it is listed in the "Images approved with this build" section with the related diff that had its image was approved + other_diff.approved = true + other_diff.approved_by = current_user + other_diff.save + end + end + end + + # Branch Build Only - Special case method for when multiple developers make changes on the same image, view with multiple diffs shows and they are allowed to + # approve either one of the old images + def approve_old_image(current_user) + logger.info "Clicked approve old image on diff: #{diff.id}" + + # Approved image should be marked back to 1 associated image because the user just took action and ruled the other images out + self.old_image.approved = true + self.old_image.save + + self.approved = true + self.approved_by = current_user + self.save + + cleanup_related_diffs current_user, false + end + + def approved_by_username + self.approved_by.nil? ? 'Unknown' : self.approved_by.display_name + end +end diff --git a/app/models/jira.rb b/app/models/jira.rb new file mode 100644 index 0000000..eec3423 --- /dev/null +++ b/app/models/jira.rb @@ -0,0 +1,108 @@ +require 'net/http' +require 'net/https' +require 'net/http/post/multipart' +require 'json' + +class Jira < ActiveRecord::Base + belongs_to :diff + validates_associated :diff + + # Create Jira with the given variables + def create_jira_request + begin + should_create_issues = VizzyConfig.instance.get_config_value(['jira', 'create_issues']) + return {success: 'Creating issues is turned off in vizzy.yaml'} unless should_create_issues + + set_priority + create_request_service + response_body = create_jira + return response_body if response_body.key?(:error) + self.jira_key = response_body['key'] + self.jira_link = "#{self.jira_base_url}/browse/#{self.jira_key}" + self.save + + add_jira_link_to_test + + upload_attachments + rescue StandardError => e + Bugsnag.notify(e) + puts "HTTP Request failed (#{e.message})" + end + end + + private + def add_jira_link_to_test + self.diff.old_image.test.jira = jira_link + self.diff.old_image.test.save + end + + def create_jira + dict = { + :fields => { + :components => [ + { + :name => "#{self.component}" + } + ], + :project => { + :key => "#{self.project}" + }, + :issuetype => { + :name => "#{self.issue_type}" + }, + :description => "#{self.description}", + :summary => "#{self.title}", + :assignee => { + # -1 assigns to default user + :name => "-1", + }, + :priority => { + :id => "#{self.priority}" + }, + } + } + body = JSON.dump(dict) + + # Create Request + req = Net::HTTP::Post.new(@path_prefix) + # Add headers + req.add_field 'Accept', 'application/json' + # Set header and body + req.add_field 'Content-Type', 'application/json' + req.body = body + + @request_service.make_request(req) + end + + def upload_attachments + upload_image_to_jira(self.diff.old_image.image.path) + upload_image_to_jira(self.diff.new_image.image.path) + upload_image_to_jira(self.diff.differences.path) + end + + def upload_image_to_jira(file_path) + req = Net::HTTP::Post::Multipart.new("#{@path_prefix}/#{self.jira_key}/attachments", :file => UploadIO.new(file_path, 'image/png')) + req.add_field("X-Atlassian-Token", "nocheck") + @request_service.make_request(req) + end + + def create_request_service + user = Rails.application.secrets.JIRA_USERNAME + pass = Rails.application.secrets.JIRA_PASSWORD + @request_service = RequestService.new(self.jira_base_url, user, pass) + @path_prefix = "/rest/api/2/issue" + end + + # 3 is major, 2 is critical, 1 is blocker + def set_priority + if self.priority.eql? 'Blocker' + self.priority = '1' + elsif self.priority.eql? 'Critical' + self.priority = '2' + elsif self.priority.eql? 'Major' + self.priority = '3' + elsif self.priority.eql? 'Trivial' + self.priority = '4' + end + end +end diff --git a/app/models/project.rb b/app/models/project.rb new file mode 100644 index 0000000..383fdd2 --- /dev/null +++ b/app/models/project.rb @@ -0,0 +1,61 @@ +class Project < ActiveRecord::Base + has_many :builds, dependent: :delete_all + serialize :plugin_settings, HashSerializer + store_accessor :plugin_settings + + def github_repo_url + if self.github_root_url.blank? || self.github_repo.blank? + nil + else + self.github_root_url + '/' + self.github_repo + end + end + + def calculate_base_images + images = [] + if self.builds.size == 0 + return images + end + self.tests.find_each do |test| + image = test.current_base_image + images.push(image) unless image.nil? + end + # Sort alphabetically + images.sort_by! { |image| image.image_file_name.downcase } + images + end + + def remove_base_images_not_uploaded_in_last_branch_build + branch_build = self.branch_builds.last + return if branch_build.nil? + base_images_to_remove = branch_build.get_base_images_not_uploaded + return if base_images_to_remove.blank? + test_ids = base_images_to_remove.map { |image| image.test_id } + TestImage.joins(:test).where(test: test_ids).find_each { |image| image.remove_image_from_base_images } + end + + def tests_ancestry_tree + tests.arrange + end + + def branch_builds + builds.where(pull_request_number: '-1', temporary: false) + end + + def pull_requests(pr_number = nil) + if pr_number.nil? + pr_builds = self.builds.where.not(pull_request_number: -1) + else + pr_builds = self.builds.where(pull_request_number: pr_number) + end + pr_builds.where(temporary: false) + end + + def tests + Test.where(project_id: self) + end + + def uncommitted_builds + builds.where(temporary: true) + end +end diff --git a/app/models/test.rb b/app/models/test.rb new file mode 100644 index 0000000..89b46e1 --- /dev/null +++ b/app/models/test.rb @@ -0,0 +1,43 @@ +class Test < ActiveRecord::Base + has_many :test_images + has_ancestry + + def current_base_image + self.test_images.where(approved: true).last + end + + def self.create_or_find(project_id, ancestry_key) + return nil if ancestry_key.blank? + test = Test.where(project_id: project_id, ancestry_key: ancestry_key).first + return test unless test.nil? + create_test_tree(project_id, get_ancestry_key_array(ancestry_key)) + end + + def self.create_test_tree(project_id, ancestry_key_array, parent = nil) + return parent if ancestry_key_array.empty? + ancestry_key = ancestry_key_array.shift + test_name = ancestry_key.split('/').last + test = Test.where(project_id: project_id, ancestry_key: ancestry_key).first + if test.nil? + test = Test.create(project_id: project_id, name: test_name, ancestry_key: ancestry_key, parent: parent) + end + create_test_tree(project_id, ancestry_key_array, test) + end + + def self.get_ancestry_key_array(ancestry_key) + test_node_data = [] + ancestry_key.split('/').each do |word| + if test_node_data.empty? + test_node_data.push(word) + else + test_string = test_node_data.last + test_node_data.push("#{test_string}/#{word}") + end + end + test_node_data + end + + def test_image_history + self.test_images.where(approved: true).sort_by {|test_img| test_img.image_updated_at}.reverse + end +end diff --git a/app/models/test_image.rb b/app/models/test_image.rb new file mode 100644 index 0000000..15e5886 --- /dev/null +++ b/app/models/test_image.rb @@ -0,0 +1,58 @@ +require 'digest/md5' +class TestImage < ActiveRecord::Base + + PAPERCLIP_BASEDIR = Rails.root + 'public/' + + has_attached_file :image, path: ':rails_root/public/visual_images/:class/:attachment/:id_partition/:style/:filename', url: '/visual_images/:class/:attachment/:id_partition/:style/:filename', styles: {thumbnail: '125x', small: '300x'} + validates_attachment :image, content_type: {content_type: ["application/octet-stream", "multipart/form-data", "image/jpg", "image/jpeg", "image/png", "image/gif"]} + has_and_belongs_to_many :base_builds, class_name: 'Build', join_table: 'builds_base_images' + has_and_belongs_to_many :successful_tests_builds, class_name: 'Build', join_table: 'builds_successful_tests' + belongs_to :build + belongs_to :test + acts_as_commontable + + def validate_md5 + file_md5 = Digest::MD5.file(self.image.path).hexdigest + provided_md5 = self.build.full_list_of_image_md5s[self.test_key] + if file_md5.blank? + :read_error + elsif provided_md5.blank? + :not_in_build + elsif file_md5 != provided_md5 + :mismatched + else + self.md5 = provided_md5 + self.save + :success + end + end + + def test=(test) + super(test) + self.test_key = self.test.ancestry_key + end + + def remove_image_from_base_images + self.approved = false + self.clear_preapproval_information(false) + self.save + end + + # Preapproved status only depends on the image_pull_request_sha being present + # We keep the pull request number around so we can view it later when it's a full approval + def preapproved? + self.image_pull_request_sha != nil + end + + def preapprove(pr_num, sha) + self.image_pull_request_sha = sha + self.image_pull_request_number = pr_num + end + + def clear_preapproval_information(keep_pr_number) + self.image_pull_request_sha = nil + unless keep_pr_number + self.image_pull_request_number = nil + end + end +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000..c522b47 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,44 @@ +class User < ActiveRecord::Base + # Include default devise modules. Others available are: + # :confirmable, :lockable, :timeoutable and :omniauthable + if VizzyConfig.instance.is_ldap_auth + devise :database_authenticatable, :rememberable, :trackable, :validatable + else + devise :database_authenticatable, :registerable, + :rememberable, :trackable, :validatable + end + + acts_as_token_authenticatable + acts_as_commontator + belongs_to :diff + after_initialize :update_user_fields + + def update_user_fields + self.username = self.email.split('@').first if self.username.blank? + self.admin = true if self.owner? + end + + def owner? + admins = Rails.application.secrets.ADMIN_EMAILS.split(',').map(&:strip) + admins.include?(email) + end + + def role + if owner? + :Owner + elsif admin? + :Admin + else + :Member + end + end + + def password_required? + false + end + + def display_name + self.username.blank? ? self.email : self.username + end +end + diff --git a/app/plugin-helpers/plugin.rb b/app/plugin-helpers/plugin.rb new file mode 100644 index 0000000..92e933a --- /dev/null +++ b/app/plugin-helpers/plugin.rb @@ -0,0 +1,32 @@ +class Plugin + include PluginBase + attr_reader :unique_id + + # Plugins must call super with the unique id + # Params: + # - unique_id: unique id to pass to super + def initialize(unique_id) + @unique_id = unique_id.to_sym + end + + # Default implementation for build_created_hook. To use this hook, define a build_created_hook(build) method in your plugin + # Params: + # - build: build that holds info that can be used for your plugins + def build_created_hook(build) + {not_implemented: 'Build Created Hook'} + end + + # Default implementation for build_committed_hook. To use this hook, define a build_committed_hook(build) method in your plugin + # Params: + # - build: build that holds info that can be used for your plugins + def build_committed_hook(build) + {not_implemented: 'Build Committed Hook'} + end + + # Default implementation for build_failed_hook. To use this hook, define a build_failed_hook(build) method in your plugin + # Params: + # - build: build that holds info that can be used for your plugins + def build_failed_hook(build) + {not_implemented: 'Build Failed Hook'} + end +end \ No newline at end of file diff --git a/app/plugin-helpers/plugin_base.rb b/app/plugin-helpers/plugin_base.rb new file mode 100644 index 0000000..0d42464 --- /dev/null +++ b/app/plugin-helpers/plugin_base.rb @@ -0,0 +1,47 @@ +module PluginBase + module ClassMethods + def repository + @repository ||= [] + end + + def inherited(klass) + repository << klass + end + end + + def self.included(klass) + klass.extend ClassMethods + end + + # Retrieves plugin settings from postgres jsonb store + # Params: + # - project: project to fetch the setting from + # - setting_symbol: setting to fetch passed in a symbol, ex :base_bamboo_build_url + def get_plugin_setting_value(project, setting_symbol) + plugin_settings = project.plugin_settings + return '' if plugin_settings.blank? || plugin_settings[unique_id].blank? + plugin_settings[unique_id][setting_symbol][:value] + end + + # Adds project settings that will be shown in the new or edit Project form. + # Params: + # - project: project to add the settings to + # - settings: hash of one or more settings that should follow the format of setting => value: '', display_name: '', and placeholder: '' so the project form can be populated correctly. + # See /app/plugins/bamboo_plugin.rb for an example + def add_plugin_settings_to_project(project, settings) + return unless project.plugin_settings[unique_id].blank? + settings = settings || {} + settings[:enabled] = enabled?(project) + project.plugin_settings[unique_id] = settings + end + + # Determines whether or not a specific plugin is enabled + # Params: + # - project: project to fetch plugin settings from + def enabled?(project) + plugin_settings = project.plugin_settings + setting_specific_hash = plugin_settings[unique_id] + return false if plugin_settings.blank? || setting_specific_hash.blank? + setting_specific_hash[:enabled] + end +end \ No newline at end of file diff --git a/app/plugin-helpers/plugin_manager.rb b/app/plugin-helpers/plugin_manager.rb new file mode 100644 index 0000000..bb9cd4b --- /dev/null +++ b/app/plugin-helpers/plugin_manager.rb @@ -0,0 +1,122 @@ +require 'singleton' + +class PluginManager + include Singleton + + # Loads plugins and adds them to the class variable @@plugin_hash + # Params: + # - project: project to load the plugins for + def for_project(project) + @@plugins_hash = {} + load_plugins(project) + yield self if block_given? + self + end + + # Runs the build created hook for all enabled plugins + # Params: + # - build: build to run the build created hook for + def run_build_created_hook(build) + run_hook(build.project) do |plugin| + plugin.build_created_hook(build) + end + end + + # Runs the build commited hook for all enabled plugins + # Params: + # - build: build to run the build commited hook for + def run_build_commited_hook(build) + run_hook(build.project) do |plugin| + plugin.build_committed_hook(build) + end + end + + # Runs the build failed hook for all enabled plugins + # Params: + # - build: build to run the build failed hook for + def run_build_failed_hook(build) + run_hook(build.project) do |plugin| + plugin.build_failed_hook(build) + end + end + + # Returns an array of all plugin classes + def plugins + plugins_hash.map {|plugin_hash| plugin_hash.last} + end + + # Returns an array of plugin classes that are enabled + def enabled_plugins(project) + plugins.select {|plugin| plugin.enabled?(project)} + end + + # Returns a hash of plugin unique_id to plugin class + def plugins_hash + @@plugins_hash + end + + # Retrieves the plugin class name + # Params: + # - unique_id: unique id of plugin to get the class name + def get_plugin_name_from_unique_id(unique_id) + @@plugins_hash[unique_id].class.name + end + + # Checks if a plugin is enabled on a project + # Params: + # - project: project to look up plugin settings + # - name: name of plugin to check -- called via PluginClass.name + def is_plugin_enabled(project, name) + for_project(project) + project.plugin_settings.each do |plugin_setting| + plugin_key = plugin_setting.first + plugin_hash = plugin_setting.second + plugin_name = get_plugin_name_from_unique_id(plugin_key) + if plugin_name == name && plugin_hash[:enabled] + return plugin_hash + end + end + nil + end + + private + + # Creates all plugins and loads them into the plugin hash. Also adds plugin settings to the projects database table + # Params: + # - project: project to add plugin settings to + def load_plugins(project) + Plugin.repository.each do |build_plugin| + id, _ = build_plugin.instance_method(:initialize).source_location + unless @@plugins_hash[id.to_sym] + plugin = build_plugin.new(id) + @@plugins_hash[id.to_sym] = plugin + plugin.add_plugin_settings_to_project(project) + end + end + end + + # Runs plugin hook for all enabled plugins + # Yield: + # - plugin: yields plugin so a specific hook can be run + def run_hook(project) + errors = [] + enabled_plugins(project).each do |plugin| + hook_result = yield(plugin) + add_to_errors_if_error(hook_result, errors) + end + {errors: errors} + end + + # Filters the hook result for not implemented and adds errors if present + # Params: + # - hook_result: the result to inspect for errors + # - errors: array containing all errors to be returned to the caller + def add_to_errors_if_error(hook_result, errors) + if hook_result.key?(:not_implemented) + # Do nothing, method not implemented for this plugin + elsif hook_result.key?(:error) + errors.push(hook_result) + end + errors + end +end \ No newline at end of file diff --git a/app/plugins/bamboo_plugin.rb b/app/plugins/bamboo_plugin.rb new file mode 100644 index 0000000..b910ae3 --- /dev/null +++ b/app/plugins/bamboo_plugin.rb @@ -0,0 +1,75 @@ +class BambooPlugin < Plugin + + def initialize(unique_id) + super(unique_id) + @username = Rails.application.secrets.BAMBOO_USERNAME + @password = Rails.application.secrets.BAMBOO_PASSWORD + end + + # Add bamboo base url project plugin settings + # Params: + # - project: project to add settings to + def add_plugin_settings_to_project(project) + super(project, { + base_bamboo_build_url: { + value: get_base_bamboo_url(project), + display_name: 'Base Bamboo Url', + placeholder: "Add base bamboo build url (e.g., 'https://bamboo.com')" + } + }) + end + + # Comment on bamboo build when Vizzy build is created + # Params: + # - build: build object containing relevant info + def build_created_hook(build) + return {dev_build: 'Dev Build, Not Running Bamboo Plugin Hook'} if build.dev_build + comment_on_build(build) + end + + private + def find_or_create_request_service + @service = @service ||= RequestService.new(@base_bamboo_build_url, @username, @password) + end + + # Add comment to bamboo build with link to the visual build + # Params: + # - build: build object containing all relevant info + def comment_on_build(build) + should_comment_on_build = VizzyConfig.instance.get_config_value(['bamboo', 'comment_on_build']) + return {success: 'Commenting on builds is turned off in vizzy.yaml'} unless should_comment_on_build + + return {error: "Bamboo Request Info Blank"} if @username.blank? || @password.blank? || build.title.blank? || build.vizzy_build_url.blank? + puts "Adding comment to bamboo with key #{build.title} with build url: #{build.vizzy_build_url }" + + + @base_bamboo_build_url = get_base_bamboo_url(build.project) + return {error: "Bamboo Build Url Blank"} if @base_bamboo_build_url.blank? + + find_or_create_request_service + + dict = { + content: build.vizzy_build_url + } + + # Create Request + request = Net::HTTP::Post.new("/rest/api/latest/result/#{build.title}/comment") + request.add_field 'Content-Type', 'application/json' + request.add_field 'Accept', 'application/json' + request.body = JSON.dump(dict) + response = @service.make_request(request) + # Don't fail the vizzy build if the bamboo response comes back with 'Not Found', just print the error + if response[:error] == "Not Found" + puts "Error: Bamboo build not found, did not successfully comment on the build." + return {success: "Ok"} + end + response + end + + # Add bammboo url from project plugin settings + # Params: + # - project: project to look for plugin settings + def get_base_bamboo_url(project) + get_plugin_setting_value(project, :base_bamboo_build_url) + end +end \ No newline at end of file diff --git a/app/plugins/jenkins_plugin.rb b/app/plugins/jenkins_plugin.rb new file mode 100644 index 0000000..ce19edb --- /dev/null +++ b/app/plugins/jenkins_plugin.rb @@ -0,0 +1,66 @@ +class JenkinsPlugin < Plugin + + def initialize(unique_id) + super(unique_id) + @username = Rails.application.secrets.JENKINS_USERNAME + @password = Rails.application.secrets.JENKINS_PASSWORD + end + + # Jenkins plugin does not require any project settings + def add_plugin_settings_to_project(project) + super(project, {}) + end + + # Update display name and description for Jenkins build when Vizzy build is created + # Params: + # - build: build object containing relevant info + def build_created_hook(build) + return {dev_build: 'Dev Build, Not Running Jenkins Plugin Hook'} if build.dev_build + update_display_name_and_description(build) + end + + private + def find_or_create_request_service + @service = @service ||= RequestService.new(@base_jenkins_url, @username, @password) + end + + # Add comment to jenkins build with link to the visual build + # Params: + # - build: build object containing all relevant info + def update_display_name_and_description(build) + should_update_description = VizzyConfig.instance.get_config_value(['jenkins', 'update_description']) + return {success: 'Updating description is turned off in vizzy.yaml'} unless should_update_description + + return {error: "Jenkins Request Info Blank"} if build.title.blank? || build.vizzy_build_url.blank? + puts "Adding description to Jenkins build with key #{build.title} with build url: #{build.vizzy_build_url }" + + @base_jenkins_url = build.url + return {error: "Jenkins Build Url Blank"} if @base_jenkins_url.blank? + + find_or_create_request_service + + data = { + Submit: "save", + json: "{ displayName: \"#{build.title}\", description: \"#{build.vizzy_build_url}\" }", + } + body = URI.encode_www_form(data) + + post_url = if build.url[-1] == '/' + "#{build.url}configSubmit" + else + "#{build.url}/configSubmit" + end + + # Create Request + request = Net::HTTP::Post.new(post_url) + request.add_field "Content-Type", "application/x-www-form-urlencoded" + request.body = body + response = @service.make_request(request) + if response[:error] == "Found" + # Jenkins returns a 302 found when updating the description + puts "Successfully updated Jenkins description." + return {success: "Ok"} + end + response + end +end \ No newline at end of file diff --git a/app/plugins/jira_plugin.rb b/app/plugins/jira_plugin.rb new file mode 100644 index 0000000..30962ee --- /dev/null +++ b/app/plugins/jira_plugin.rb @@ -0,0 +1,40 @@ +class JiraPlugin < Plugin + + def initialize(unique_id) + super(unique_id) + end + + def add_plugin_settings_to_project(project) + super(project, { + jira_base_url: { + value: get_jira_base_url(project), + display_name: 'Base Jira Url', + placeholder: "Add jira root url (e.g., 'https://jira.com')" + }, + jira_project: { + value: get_jira_project(project), + display_name: 'Jira Project', + placeholder: "Add jira project to file tickets (e.g., 'MOBILE')" + }, + jira_component: { + value: get_jira_component(project), + display_name: 'Jira Component', + placeholder: "Add jira component to file tickets (e.g., 'Visual Automation')" + } + }) + end + + private + + def get_jira_base_url(project) + get_plugin_setting_value(project, :jira_base_url) + end + + def get_jira_project(project) + get_plugin_setting_value(project, :jira_project) + end + + def get_jira_component(project) + get_plugin_setting_value(project, :jira_component) + end +end \ No newline at end of file diff --git a/app/plugins/slack_plugin.rb b/app/plugins/slack_plugin.rb new file mode 100644 index 0000000..e75108e --- /dev/null +++ b/app/plugins/slack_plugin.rb @@ -0,0 +1,167 @@ +class SlackPlugin < Plugin + + def initialize(unique_id) + super(unique_id) + end + + # Add slack channel to project plugin settings + # Params: + # - project: project to add settings to + def add_plugin_settings_to_project(project) + super(project, { + slack_channel: { + value: get_slack_channel(project), + display_name: 'Slack Channel', + placeholder: "Add slack channel to send build results to (e.g., '#build-status')" + } + }) + end + + # send slack message when a build commits + # Params: + # - build: build object containing relevant info + def build_committed_hook(build) + return {dev_build: 'Dev Build, Not Running Bamboo Plugin Hook'} if build.dev_build + send_build_commit_slack_update(build) + end + + # send slack message when a build fails + # Params: + # - build: build object containing relevant info + def build_failed_hook(build) + return {dev_build: 'Dev Build, Not Running Bamboo Plugin Hook'} if build.dev_build + send_build_commit_slack_update(build) + end + + private + def find_or_create_request_service + @service = @service ||= RequestService.new('https://hooks.slack.com') + end + + # Get slack channel from project plugin settings + # Params: + # - project: project to look for plugin settings + def get_slack_channel(project) + get_plugin_setting_value(project, :slack_channel) + end + + # Send build commit slack message + # Params: + # - build: build object containing relevant info + def send_build_commit_slack_update(build) + should_create_issues = VizzyConfig.instance.get_config_value(['slack', 'send_messages']) + return {success: 'Sending messages is turned off in vizzy.yaml'} unless should_create_issues + + state = build.current_state + links = get_links(build) + channel = get_channel(build) + message = get_message(build, state) + + send_message(channel, build.project.name, build.branch_name, message, links, state[:status] == :failure || state[:status] == :forced_failure) + end + + # Get links for build system, visual automation, and github + # Params: + # - build: build object containing relevant info + def get_links(build) + links = [] + unless build.url.blank? + links.append({url: build.url, text: 'Build'}) + end + links.append({url: build.vizzy_build_url, text: 'Visual Automation'}) + unless build.is_branch_build + links.append({url: "#{build.project.github_repo_url}/pull/#{build.pull_request_number}", text: 'Github'}) + end + links + end + + # Get slack channel. if build is a pull request, the slack channel will be the user who created the build. Otherwise channel will be retrieved from the plugin settings + # Params: + # - build: build object containing username and project + def get_channel(build) + channel = '' + if build.is_branch_build + channel = get_slack_channel(build.project) + elsif !build.username.blank? + channel = "@#{build.username}" + end + channel + end + + # Get a brief summary of build state + # Params: + # - build: build object containing relevant info + # - state: hash of the current state of the build, contains :message and :status + def get_message(build, state) + message = "" + if state[:status] == :forced_failure + if build.is_branch_build + message += "The visual build has failed!" + else + message += "Your pull request visual build has failed!" + end + message += " #{state[:message]}" + elsif build.is_branch_build + message += "A visual build just successfully finished with #{state[:message]}" + if state[:status] == :success + message += '! :smile2:' + else + message += '. Please fix or approve them.' + end + else + message += "Your pull request visual build successfully finished with #{state[:message]}" + if state[:status] == :success + message += '! Great job, if the other parts of your build have completed successfully, you may check in your code!' + else + message += '. Please fix or approve them.' + end + end + message + end + + # Sends a slack message + # Params: + # - channel: channel to send the slack message to + # - project_name: title of the slack message + # - branch_name: source control branch name if present, can be nil + # - message: brief summary of build state + # - links: an array of hashes representing a link, with keys 'text', and 'url' + # - failure: whether or not the build failed, determines message color + def send_message(channel, project_name, branch_name, message, links, failure=false) + if channel.blank? + return {error: "Slack Channel Blank"} + end + + color = failure ? '#B0171F' : '#7CD197' + pretext = message + fallback = message + 'Links: ' + links.collect {|link| link[:text] + ': ' + link[:url]}.join(', ') + text = links.collect {|link| "<#{link[:url]}|#{link[:text]}>" }.join("\n") + title = project_name + unless branch_name.nil? + title += " - #{branch_name}" + end + + puts "Sending slack message to user: #{channel} with message: #{pretext}" + dict = { + channel: "#{channel}", + username: "Vizzy", + icon_emoji: ":vizzy:", + attachments: [ + { + fallback: fallback, + pretext: pretext, + title: title, + text: text, + color: color + } + ] + } + + find_or_create_request_service + request = Net::HTTP::Post.new(Rails.application.secrets.SLACK_WEBHOOK) + request.add_field 'Content-Type', 'application/json' + request.add_field 'Accept', 'application/json' + request.body = JSON.dump(dict) + @service.make_request(request) + end +end \ No newline at end of file diff --git a/app/serializers/hash_serializer.rb b/app/serializers/hash_serializer.rb new file mode 100644 index 0000000..b5cc529 --- /dev/null +++ b/app/serializers/hash_serializer.rb @@ -0,0 +1,13 @@ +class HashSerializer + def self.dump(hash) + hash.to_json + end + + def self.load(hash) + if hash.blank? + {} + else + JSON.parse(hash) + end.deep_symbolize_keys! + end +end \ No newline at end of file diff --git a/app/services/github_service.rb b/app/services/github_service.rb new file mode 100644 index 0000000..9fd27fb --- /dev/null +++ b/app/services/github_service.rb @@ -0,0 +1,98 @@ +class GithubService + def initialize(server_url, repo) + @token = Rails.application.secrets.GITHUB_AUTH_TOKEN + @path_prefix = "/api/v3/repos/" + repo + @request_service = RequestService.new(server_url, @token, "x-oauth-basic") + end + + # Static method that takes a block with the service, also runs some validations + def self.run(server_url, repo) + if server_url.blank? or repo.blank? + return false + end + + service = GithubService.new(server_url, repo) + yield(service) + true + end + + # Sends a status without a build + def send_project_status(project, url, hash, status, description) + path = @path_prefix + "/statuses/#{hash}" + context = project.github_status_context + dict = { + target_url: url, + state: status, + description: description, + context: context + } + make_request(path, false, dict) + end + + def send_status(build, status, description) + send_project_status(build.project, build.vizzy_build_url, build.commit_sha, status, description) + end + + def github_commits(number_of_days_ago, from_commit) + current_page = 0 + all_commits = Array.new + begin + current_page += 1 + json_response = pagination_github_commits(number_of_days_ago, from_commit, current_page) + if json_response.is_a?(Hash) && json_response.key?(:error) + puts "Failed request, returning empty array for git commits" + return [] + end + shas_array = json_response.collect { |commit| commit['sha'] } + all_commits.concat(shas_array) + end while json_response.size == 100 + + all_commits + end + + # Used for testing preapprovals + def most_recent_github_commits + path = @path_prefix + "/commits" + json_response = make_request(path, true, nil) + if json_response.is_a?(Hash) && json_response.key?(:error) + puts "Failed request, returning empty array for git commits" + return [] + end + json_response.collect { |commit| commit['sha'] } + end + + def user_and_branch_for_pull_request(pr_num) + pr = make_request(@path_prefix + "/pulls/#{pr_num}", true, nil) + # Validate we didn't get an error + if pr.key?(:error) + raise("Failed to get user and branch information for pull request ##{pr_num}") + end + + branch = pr['head']['ref'] + # Use ldap info instead of 'login' so we get the official AD username without needing a whitelist for names with hyphens + # Looking at you 'erh-li.shen' + ldap_info = pr['user']['ldap_dn'] + user = /CN=(.*?),/.match(ldap_info).captures[0] + {branch: branch, user: user} + end + + private + def pagination_github_commits(number_of_days_ago, from_commit, page_number) + days_ago = number_of_days_ago.days.ago.to_time.iso8601 + + path = @path_prefix + "/commits?since=#{days_ago}&sha=#{from_commit}&per_page=100&page=#{page_number}" + make_request(path, true, nil) + end + + def make_request(path, is_get, json_dict) + request = is_get ? Net::HTTP::Get.new(path) : Net::HTTP::Post.new(path) + request.add_field 'Content-Type', 'application/json' + request.add_field 'Accept', 'application/json' + unless json_dict.nil? + body = JSON.dump(json_dict) + request.body = body + end + + @request_service.make_request(request) + end +end diff --git a/app/services/request_service.rb b/app/services/request_service.rb new file mode 100644 index 0000000..b5a2142 --- /dev/null +++ b/app/services/request_service.rb @@ -0,0 +1,53 @@ + +# Helper class to encapsulate common request methods and authentication +class RequestService + def initialize(server_url, user=nil, pass=nil) + uri = URI(server_url) + @http = Net::HTTP.new(uri.host, uri.port) + if server_url.start_with?("https") + @http.use_ssl = true + @http.verify_mode = OpenSSL::SSL::VERIFY_PEER + end + + @user = user + @pass = pass + end + + def make_request(req) + unless @user.blank? and @pass.blank? + req.basic_auth(@user, @pass) + end + begin + response = @http.request(req) + puts "Response HTTP Status Code: #{response.code}" + puts "Response HTTP Response Body: #{response.body}" + handle_response(response) + rescue StandardError => e + Bugsnag.notify(e) + puts "HTTP Request failed (#{e.message})" + end + end + + def handle_response(response) + case response + when Net::HTTPSuccess + if response.body.blank? + {success: "Empty Response Body"} + elsif response.body == 'ok' + {success: "Ok"} + else + JSON.parse(response.body) + end + when Net::HTTPUnauthorized + {error: "Unauthorized: #{response.message}"} + when Net::HTTPServerError + {error: "Server Error: #{response.message}"} + else + if response.message.blank? + {error: response.body} + else + {error: response.message} + end + end + end +end \ No newline at end of file diff --git a/app/views/application/_favicon.html.erb b/app/views/application/_favicon.html.erb new file mode 100644 index 0000000..ed892b2 --- /dev/null +++ b/app/views/application/_favicon.html.erb @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/app/views/builds/_form.html.erb b/app/views/builds/_form.html.erb new file mode 100644 index 0000000..9ba321c --- /dev/null +++ b/app/views/builds/_form.html.erb @@ -0,0 +1,23 @@ +<%= form_for(@build) do |f| %> + <%= render 'layouts/error_messages', object: f.object %> + +
+ <%= f.collection_select :project_id, Project.all, :id, :name, prompt: true %> + <%= f.label :project_id, 'Project' %> +
+
+ <%= f.label :url %>
+ <%= f.text_field :url %> +
+
+ <%= f.label :temporary %>
+ <%= f.check_box :temporary %> +
+
+ <%= f.label :title %>
+ <%= f.text_field :title %> +
+
+ <%= f.submit %> +
+<% end %> diff --git a/app/views/builds/edit.html.erb b/app/views/builds/edit.html.erb new file mode 100644 index 0000000..0d52988 --- /dev/null +++ b/app/views/builds/edit.html.erb @@ -0,0 +1,6 @@ +

Edit Build

+ +<%= render 'form' %> + +<%= link_to 'Show', @build %> | +<%= link_to 'Back', builds_path %> diff --git a/app/views/builds/index.html.erb b/app/views/builds/index.html.erb new file mode 100644 index 0000000..d29d53b --- /dev/null +++ b/app/views/builds/index.html.erb @@ -0,0 +1,40 @@ +<% breadcrumb :builds %> +

Recent Builds

+ + + + + + + + + + + + + + + + + <% @builds.last(300).sort_by {|build| build.created_at}.reverse_each do |build| %> + + + + + + <% if build.pull_request_number == "-1" %> + + <% else %> + + <% end %> + <% if build.branch_name.blank? %> + + <% else %> + + <% end %> + + + + <% end %> + +
IDTitleProject IDHashPull Request NumberBranch NameDev BuildTemporary
<%= link_to build.id, build_path(build.id) %><%= link_to build.title, build_path(build.id) %><%= link_to build.project.id, project_path(build.project.id) %><%= link_to build.commit_sha, build_path(build.id) %>-<%= link_to build.pull_request_number, build_path(build.id) %>-<%= link_to build.branch_name, build_path(build.id) %><%= build.dev_build? %><%= build.temporary? %>
\ No newline at end of file diff --git a/app/views/builds/index.json.jbuilder b/app/views/builds/index.json.jbuilder new file mode 100644 index 0000000..1238149 --- /dev/null +++ b/app/views/builds/index.json.jbuilder @@ -0,0 +1,4 @@ +json.array!(@builds) do |build| + json.extract! build, :id, :url, :temporary, :title + json.url build_url(build, format: :json) +end diff --git a/app/views/builds/new.html.erb b/app/views/builds/new.html.erb new file mode 100644 index 0000000..878618d --- /dev/null +++ b/app/views/builds/new.html.erb @@ -0,0 +1,5 @@ +

New Build

+ +<%= render 'form' %> + +<%= link_to 'Back', builds_path %> diff --git a/app/views/builds/show.html.erb b/app/views/builds/show.html.erb new file mode 100644 index 0000000..c506789 --- /dev/null +++ b/app/views/builds/show.html.erb @@ -0,0 +1,104 @@ +<% breadcrumb :build, @build %> +
+ <% if @build.branch_name.nil? or @build.username.nil? %> + <%= image_tag("vizzy-text.svg", class: "img-responsive pull-left vizzy-build-bottom-spacing") %> + <% end %> +

+ <% if @build.dev_build? %> + Dev Build - No Action Can Be Taken. New tests will not be shown. + <% elsif @build.branch_name.nil? or @build.username.nil? %> + <% else %> + <%= @build.branch_name %> + + (<%= @build.username %>) + + <% end %> +

+ <%= button_to 'Current Base Images', base_images_project_path(@build.project), :class => 'btn-primary btn pull-right' %> +
+
+
+

Build Info

+
+
+ <% if @build.can_approve_images && @unapproved_diffs.size > 0 %> + <%= button_to 'Approve All Images', approve_all_images_build_path(@build), :class => 'btn-success btn pull-right' %> + <% end %> + <% if @build.title? %> + + <%= link_to @build.title, @build.url %> + +
+ <% end %> + + <%= @build.formatted_created_at_time %> + +
+ Project: <%= @build.project.name %> +
+ <% if !@build.is_branch_build && !@build.dev_build %> +
+ Pull Request: <%= link_to("#{@build.project.github_repo_url}/pull/#{@build + .pull_request_number}", "#{@build.project.github_repo_url}/pull/#{@build.pull_request_number}") %> +
+ <% end %> + <% if @build.commit_sha? %> +
Sha: <%= @build.commit_sha %>
+ <% end %> +
<%= @build.num_of_images_in_build %> images checked
+
<%= @build.diffs.size %> difference(s) found
+
<%= @new_tests.size %> new test(s) added
+ <% unless @missing_tests.blank? %> + <%= link_to "#{@missing_tests.size} missing test(s)", missing_tests_build_path, :class => 'alert-link link-danger' %> + <% end %> + <% unless @successful_tests.blank? %> + <%= link_to "#{@successful_tests.size} successful test(s)", successful_tests_build_path, :class => 'alert-link link-success' %> + <% end %> +
+
+ +<% if @build.temporary? %> +
This build has not been committed. Uploads may still be in progress.
+<% elsif !@build.failure_message.blank? %> +
Build Failed: <%= @build.failure_message %>. + <%= link_to 'Please look at the CI build to diagnose the issue.', @build.url, :class => 'alert-link' %> +
+<% elsif @build.num_of_images_in_build == 0 %> +
No images were uploaded to this build, there was an issue generating or uploading + the images in the build. <%= link_to 'Please look at the CI build to diagnose the issue.', @build.url, :class => 'alert-link' %> +
+<% elsif @unapproved_diffs.size > 0 %> + <% if @build.dev_build? %> + <% link_text = 'Click here to look through them.' %> + <% else %> + <% link_text = 'Please look through them to approve them.' %> + <% end %> +
This build has <%= @unapproved_diffs.size %> unapproved visual + difference(s). <%= link_to link_text, diff_path(@unapproved_diffs.first), :class => 'alert-link' %>
+<% elsif @approved_diffs.size > 0 %> +
All visual differences in this build have been approved. You can review what was + changed below. +
+<% else %> +
No visual differences were found between this build and the base images.
+<% end %> + +<% if @unapproved_diffs.size > 0 %> + <% title_text = "#{pluralize(@unapproved_diffs.size, 'diff')}" %> + <% if !@build.dev_build? %> + <% title_text += ' waiting for approval' %> + <% end %> + <%= render partial: 'layouts/diffs_panel', locals: {title: title_text, diffs: @unapproved_diffs, show_approver: false, panel_class: "panel-warning", panel_id: "diffs_images"} %> +<% end %> + +<% if @new_tests.size > 0 %> + <%= render partial: 'layouts/test_images_panel', locals: {title: "#{pluralize(@new_tests.size, 'new test')} added", images: @new_tests, panel_class: "panel-success", panel_id: "new_images"} %> +<% end %> + +<% if @approved_diffs.size > 0 %> + <%= render partial: 'layouts/diffs_panel', locals: {title: "#{pluralize(@approved_diffs.size, 'diff')} approved", diffs: @approved_diffs, show_approver: true, panel_class: "panel-success", panel_id: "approved_images"} %> +<% end %> + +<%= commontator_thread(@build) %> +
+
diff --git a/app/views/builds/show.json.jbuilder b/app/views/builds/show.json.jbuilder new file mode 100644 index 0000000..0de84c1 --- /dev/null +++ b/app/views/builds/show.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! @build, :id, :dev_build, :commit_sha, :pull_request_number, :url, :temporary, :title, :created_at, :updated_at, :image_md5s +json.base_image_count @build.base_images.size \ No newline at end of file diff --git a/app/views/devise/confirmations/new.html.erb b/app/views/devise/confirmations/new.html.erb new file mode 100644 index 0000000..a5920ea --- /dev/null +++ b/app/views/devise/confirmations/new.html.erb @@ -0,0 +1,18 @@ +<%= render 'layouts/error_messages', object: resource %> +
+
+

<%= t('.resend_confirmation_instructions', :default => 'Resend confirmation instructions') %>

+
+
+ <%= form_for(resource, :as => resource_name, :url => confirmation_path(resource_name), :html => { :method => :post, role: "form" }) do |f| %> +
+ <%= f.label :email %> + <%= f.email_field :email, autofocus: true, class: "form-control" %> +
+ + <%= f.submit t('.resend_confirmation_instructions', :default => 'Resend confirmation instructions'), class: "btn btn-primary" %> + <% end %> +
+
+ +<%= render "devise/shared/links" %> diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb new file mode 100644 index 0000000..ef13ced --- /dev/null +++ b/app/views/devise/mailer/confirmation_instructions.html.erb @@ -0,0 +1,6 @@ +

<%= t('.greeting', :recipient => @resource.email, :default => "Welcome #{@resource.email}!") %>

+ +

<%= t('.instruction', :default => "You can confirm your account email through the link below:") %>

+ +

<%= link_to t('.action', :default => "Confirm my account"), + confirmation_url(@resource, :confirmation_token => @token, locale: I18n.locale) %>

diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb new file mode 100644 index 0000000..611cacb --- /dev/null +++ b/app/views/devise/mailer/reset_password_instructions.html.erb @@ -0,0 +1,8 @@ +

<%= t('.greeting', :recipient => @resource.email, :default => "Hello #{@resource.email}!") %>

+ +

<%= t('.instruction', :default => "Someone has requested a link to change your password, and you can do this through the link below.") %>

+ +

<%= link_to t('.action', :default => "Change my password"), edit_password_url(@resource, :reset_password_token => @token, locale: I18n.locale) %>

+ +

<%= t('.instruction_2', :default => "If you didn't request this, please ignore this email.") %>

+

<%= t('.instruction_3', :default => "Your password won't change until you access the link above and create a new one.") %>

diff --git a/app/views/devise/mailer/unlock_instructions.html.erb b/app/views/devise/mailer/unlock_instructions.html.erb new file mode 100644 index 0000000..d3e53e6 --- /dev/null +++ b/app/views/devise/mailer/unlock_instructions.html.erb @@ -0,0 +1,7 @@ +

<%= t('.greeting', :recipient => @resource.email, :default => "Hello #{@resource.email}!") %>

+ +

<%= t('.message', :default => "Your account has been locked due to an excessive amount of unsuccessful sign in attempts.") %>

+ +

<%= t('.instruction', :default => "Click the link below to unlock your account:") %>

+ +

<%= link_to t('.action', :default => "Unlock my account"), unlock_url(@resource, :unlock_token => @resource.unlock_token, locale: I18n.locale) %>

diff --git a/app/views/devise/passwords/edit.html.erb b/app/views/devise/passwords/edit.html.erb new file mode 100644 index 0000000..684ab02 --- /dev/null +++ b/app/views/devise/passwords/edit.html.erb @@ -0,0 +1,24 @@ +<%= render 'layouts/error_messages', object: resource %> +
+
+

<%= t('.change_your_password', :default => 'Change your password') %>

+
+
+ <%= form_for(resource, :as => resource_name, :url => password_path(resource_name), :html => { :method => :put, role: "form" }) do |f| %> + <%= f.hidden_field :reset_password_token %> + +
+ <%= f.label :password, t('.new_password', :default => 'New password') %> + <%= f.password_field :password, autofocus: true, class: "form-control" %> +
+ +
+ <%= f.label :password_confirmation, t('.confirm_new_password', :default => 'Confirm new password') %> + <%= f.password_field :password_confirmation, class: "form-control" %> +
+ + <%= f.submit t('.change_my_password', :default => 'Change my password'), class: "btn btn-primary" %> + <% end %> +
+
+<%= render "devise/shared/links" %> diff --git a/app/views/devise/passwords/new.html.erb b/app/views/devise/passwords/new.html.erb new file mode 100644 index 0000000..0996649 --- /dev/null +++ b/app/views/devise/passwords/new.html.erb @@ -0,0 +1,17 @@ +<%= render 'layouts/error_messages', object: resource %> +
+
+

<%= t('.forgot_your_password', :default => 'Forgot your password?') %>

+
+
+ <%= form_for(resource, :as => resource_name, :url => password_path(resource_name), :html => { :method => :post, role: "form" }) do |f| %> +
+ <%= f.label :email %> + <%= f.email_field :email, autofocus: true, class: "form-control" %> +
+ + <%= f.submit t('.send_me_reset_password_instructions', :default => "Send me reset password instructions"), class: "btn btn-primary" %> + <% end %> +
+
+<%= render "devise/shared/links" %> diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb new file mode 100644 index 0000000..14e4ee2 --- /dev/null +++ b/app/views/devise/registrations/edit.html.erb @@ -0,0 +1,31 @@ +<%= render 'layouts/error_messages', object: resource %> +
+
+

<%= t('.title', :resource => resource_class.model_name.human , :default => 'Edit #{resource_name.to_s.humanize}') %>

+
+
+ <%= form_for(resource, :as => resource_name, :url => registration_path(resource_name), :html => { :method => :put }) do |f| %> +
+ <%= f.label :email %> + <%= f.email_field :email, autofocus: true, class: "form-control" %> +
+
+ <%= f.label :password %> (<%= t('.leave_blank_if_you_don_t_want_to_change_it', :default => "leave blank if you don't want to change it") %>)
+ <%= f.password_field :password, :autocomplete => "off", class: "form-control" %> +
+
+ <%= f.label :password_confirmation %>
+ <%= f.password_field :password_confirmation, class: "form-control" %> +
+
+ <%= f.label :current_password %> (<%= t('.we_need_your_current_password_to_confirm_your_changes', :default => 'we need your current password to confirm your changes') %>) + <%= f.password_field :current_password, class: "form-control" %> +
+ <%= f.submit t('.update', :default => "Update"), class: "btn btn-primary" %> + <% end %> +
+
+ +

<%= t('.unhappy', :default => 'Unhappy') %>? <%= link_to t('.cancel_my_account', :default => "Cancel my account"), registration_path(resource_name), :data => { :confirm => t('.are_you_sure', :default => "Are you sure?") }, :method => :delete %>.

+ +<%= link_to "Back", :back %> diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb new file mode 100644 index 0000000..a22d297 --- /dev/null +++ b/app/views/devise/registrations/new.html.erb @@ -0,0 +1,24 @@ +<%= render 'layouts/error_messages', object: resource %> +
+
+

<%= t('.sign_up', :default => "Sign up") %>

+
+
+ <%= form_for(resource, :as => resource_name, :url => registration_path(resource_name), html: { role: "form" }) do |f| %> +
+ <%= f.label :email %> + <%= f.email_field :email, autofocus: true, class: "form-control" %> +
+
+ <%= f.label :password %>
+ <%= f.password_field :password, placeholder: "Must be 12+ characters", required: true, class: "form-control" %> +
+
+ <%= f.label :password_confirmation %> + <%= f.password_field :password_confirmation, required: true, class: "form-control" %> +
+ <%= f.submit t('.sign_up', :default => "Sign up"), class: "btn btn-primary" %> + <% end %> +
+
+<%= render "devise/shared/links" %> diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb new file mode 100644 index 0000000..94a0532 --- /dev/null +++ b/app/views/devise/sessions/new.html.erb @@ -0,0 +1,36 @@ +
+
+
+
+

<%= t('.log_in', :default => "Log In") %>

+
+
+ <%= form_for(resource, as: resource_name, url: session_path(resource_name), html: {role: "form"}) do |f| %> +
+ <%= f.label :email %> + <%= f.text_field :email, autofocus: true, class: "form-control" %> +
+
+ <%= f.label :password %> + <% if VizzyConfig.instance.is_ldap_auth %> + <% password_placeholder = VizzyConfig.instance.get_config_value(['devise', 'password_placeholder']) %> + <%= f.password_field :password, autocomplete: "off", placeholder: password_placeholder, class: "form-control" %> + <% else %> + <%= f.password_field :password, autocomplete: "off", required: true, class: "form-control" %> + <% end %> +
+ <% if devise_mapping.rememberable? %> +
+ +
+ <% end %> + <%= f.submit t('.log_in', :default => "Log In"), class: "btn btn-primary" %> + <% end %> +
+
+ <%= render "devise/shared/links" %> +
+
diff --git a/app/views/devise/shared/_links.erb b/app/views/devise/shared/_links.erb new file mode 100644 index 0000000..f6ecca0 --- /dev/null +++ b/app/views/devise/shared/_links.erb @@ -0,0 +1,25 @@ +<%- if controller_name != 'sessions' %> +<%= link_to t(".sign_in", :default => "Sign in"), new_session_path(resource_name) %>
+<% end -%> + +<%- if devise_mapping.registerable? && controller_name != 'registrations' %> +<%= link_to t(".sign_up", :default => "Sign up"), new_registration_path(resource_name) %>
+<% end -%> + +<%- if devise_mapping.recoverable? && controller_name != 'passwords' %> +<%= link_to t(".forgot_your_password", :default => "Forgot your password?"), new_password_path(resource_name) %>
+<% end -%> + +<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> +<%= link_to t('.didn_t_receive_confirmation_instructions', :default => "Didn't receive confirmation instructions?"), new_confirmation_path(resource_name) %>
+<% end -%> + +<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> +<%= link_to t('.didn_t_receive_unlock_instructions', :default => "Didn't receive unlock instructions?"), new_unlock_path(resource_name) %>
+<% end -%> + +<%- if devise_mapping.omniauthable? %> + <%- resource_class.omniauth_providers.each do |provider| %> + <%= link_to t('.sign_in_with_provider', :provider => provider.to_s.titleize, :default => "Sign in with #{provider.to_s.titleize}"), omniauth_authorize_path(resource_name, provider) %>
+ <% end -%> +<% end -%> diff --git a/app/views/devise/shared/_links.html.erb b/app/views/devise/shared/_links.html.erb new file mode 100644 index 0000000..cd795ad --- /dev/null +++ b/app/views/devise/shared/_links.html.erb @@ -0,0 +1,25 @@ +<%- if controller_name != 'sessions' %> + <%= link_to "Log in", new_session_path(resource_name) %>
+<% end -%> + +<%- if devise_mapping.registerable? && controller_name != 'registrations' %> + <%= link_to "Sign up", new_registration_path(resource_name) %>
+<% end -%> + +<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> + <%= link_to "Forgot your password?", new_password_path(resource_name) %>
+<% end -%> + +<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> + <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
+<% end -%> + +<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> + <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
+<% end -%> + +<%- if devise_mapping.omniauthable? %> + <%- resource_class.omniauth_providers.each do |provider| %> + <%= link_to "Sign in with #{provider.to_s.titleize}", omniauth_authorize_path(resource_name, provider) %>
+ <% end -%> +<% end -%> diff --git a/app/views/devise/unlocks/new.html.erb b/app/views/devise/unlocks/new.html.erb new file mode 100644 index 0000000..1e020e2 --- /dev/null +++ b/app/views/devise/unlocks/new.html.erb @@ -0,0 +1,16 @@ +<%= render 'layouts/error_messages', object: resource %> +
+
+

<%= t('.resend_unlock_instructions', :default => "Resend unlock instructions") %>

+
+
+ <%= form_for(resource, :as => resource_name, :url => unlock_path(resource_name), :html => { :method => :post, html: { role: "form" } }) do |f| %> +
+ <%= f.label :email %> + <%= f.email_field :email, autofocus: true, class: "form-control" %> +
+ <%= f.submit t('.resend_unlock_instructions', :default => "Resend unlock instructions"), class: "btn btn-primary"%> + <% end %> +
+
+<%= render "devise/shared/links" %> diff --git a/app/views/diffs/_form.html.erb b/app/views/diffs/_form.html.erb new file mode 100644 index 0000000..067d138 --- /dev/null +++ b/app/views/diffs/_form.html.erb @@ -0,0 +1,15 @@ +<%= form_for(@diff) do |f| %> + <%= render 'layouts/error_messages', object: f.object %> + +
+ <%= f.label :old_image_id %>
+ <%= f.text_field :old_image_id %> +
+
+ <%= f.label :new_image_id %>
+ <%= f.text_field :new_image_id %> +
+
+ <%= f.submit %> +
+<% end %> diff --git a/app/views/diffs/edit.html.erb b/app/views/diffs/edit.html.erb new file mode 100644 index 0000000..2f20c6e --- /dev/null +++ b/app/views/diffs/edit.html.erb @@ -0,0 +1,6 @@ +

Edit Diff

+ +<%= render 'form' %> + +<%= link_to 'Show', @diff %> | +<%= link_to 'Back', diffs_path %> diff --git a/app/views/diffs/index.html.erb b/app/views/diffs/index.html.erb new file mode 100644 index 0000000..02bb0ee --- /dev/null +++ b/app/views/diffs/index.html.erb @@ -0,0 +1,31 @@ +<% breadcrumb :diffs %> +

Diffs

+ + + + + + + + + + + + <% @diffs.last(300).each do |diff| %> + + + + + + <% end %> + +
Old ImageNew ImageDifferences
<%= link_to diff do %> + <%= image_tag diff.old_image.image.url(:small) %> + <% end %> + <%= link_to diff do %> + <%= image_tag diff.new_image.image.url(:small) %> + <% end %> + <%= link_to diff do %> + <%= image_tag diff.differences.url(:small) %> + <% end %> +
\ No newline at end of file diff --git a/app/views/diffs/index.json.jbuilder b/app/views/diffs/index.json.jbuilder new file mode 100644 index 0000000..f8b3cd1 --- /dev/null +++ b/app/views/diffs/index.json.jbuilder @@ -0,0 +1,6 @@ +json.array!(@diffs) do |diff| + json.extract! diff, :id, :old_image_id, :new_image_id + json.diff_url diff_url(diff) + json.build_url build_url(diff.build) +end + diff --git a/app/views/diffs/new.html.erb b/app/views/diffs/new.html.erb new file mode 100644 index 0000000..08279d7 --- /dev/null +++ b/app/views/diffs/new.html.erb @@ -0,0 +1,5 @@ +

New diff

+ +<%= render 'form' %> + +<%= link_to 'Back', diffs_path %> diff --git a/app/views/diffs/show.html.erb b/app/views/diffs/show.html.erb new file mode 100644 index 0000000..95b01f6 --- /dev/null +++ b/app/views/diffs/show.html.erb @@ -0,0 +1,109 @@ +<% breadcrumb :diff, @diff %> +<% count = 0 %> +<% @diff.build.diffs.each {|diff| %> + <% if diff.old_image.test_key == @diff.old_image.test_key %> + <% count += 1 %> + <% end %> +<% } %> + +<% jira_plugin_hash = PluginManager.instance.is_plugin_enabled(@diff.build.project, JiraPlugin.name) %> + +<% @diff.build.diffs.each {|diff| %> + <% if diff.old_image.test_key == @diff.old_image.test_key %> +
+
+

Differences

<%= image_tag diff.differences.url, :class => 'img-responsive' %> +
+
+

Old Image

<%= image_tag diff.old_image.image.url, :class => 'img-responsive' %> +
+
+

New Image

<%= image_tag diff.new_image.image.url, :class => 'img-responsive' %> +
+
+

Info

+ <%= form_for(diff.old_image.test) do |f| %> + <% if diff.old_image.test.errors.any? %> +
+

<%= pluralize(diff.old_image.test.errors.count, "error") %> prohibited this test from being + saved:

+ +
    + <% diff.old_image.test.errors.full_messages.each do |message| %> +
  • <%= message %>
  • + <% end %> +
+
+ <% end %> +
+
+ <% unless jira_plugin_hash.nil? %> +
+ + <%= f.url_field :jira, class: 'form-control', placeholder: 'Add a Jira link' %> +
+ <% end %> +
+ + <%= f.url_field :pull_request_link, class: 'form-control', placeholder: 'Add a pull request link' %> +
+
+ + <%= f.text_area :comment, class: 'form-control', rows: '3', placeholder: 'Add a comment...' %> + <% unless diff.old_image.test.comment.blank? %> + + <% end %> +
+
+ <%= f.submit 'Save', :class => 'btn btn-success' %> +
+
+ <% end %> + <% if count > 1 %> + <% unless diff.build.temporary? %> +

+ <%= button_to 'Approve This Old Image', multiple_diff_approval_diff_path(diff), :class => 'btn-success btn vertical-center' %> +

+ <% end %> + +

+ + Old Image Pre-Approved By: + +

+ <% diff.old_image.build.approved_diffs.each {|approved_diff| %> + <% if approved_diff.new_image.test_key == diff.old_image.test_key %> + <%= "User: #{approved_diff.approved_by_username}" %> + <% end %> + <% } %> +

+ Pull + Request: <%= link_to("#{diff.build.project.github_repo_url}/pull/#{diff.old_image.image_pull_request_number}", "#{diff.build.project.github_repo_url}/pull/#{diff.old_image.image_pull_request_number}") %> +

+ <% end %> +
+
+ +
+ <% end %> +<% } %> + + diff --git a/app/views/diffs/show.json.jbuilder b/app/views/diffs/show.json.jbuilder new file mode 100644 index 0000000..25ede1a --- /dev/null +++ b/app/views/diffs/show.json.jbuilder @@ -0,0 +1 @@ +json.extract! @diff, :id, :old_image_id, :new_image_id, :created_at, :updated_at diff --git a/app/views/jiras/_form.html.erb b/app/views/jiras/_form.html.erb new file mode 100644 index 0000000..453dcc8 --- /dev/null +++ b/app/views/jiras/_form.html.erb @@ -0,0 +1,44 @@ +<%= form_for(@jira) do |f| %> + <%= render 'layouts/error_messages', object: f.object %> + +
+
+ <%= f.text_field :project, readonly: true, style: 'width:500px' %> +
+ +
+ + <%= f.text_field :title, class: 'form-control', placeholder: 'Add title', style: 'width:500px' %> +
+ +
+
+ <%= f.text_field :component, readonly: true, style: 'width:500px' %> +
+ +
+
+ <%= f.select :issue_type, ['Bug', 'Task', 'Improvement'], style: 'width:500px' %> +
+ +
+
+ <%= f.select :priority, ['Critical', 'Major', 'Blocker', 'Trivial'], style: 'width:500px' %> +
+ +
+ + <%= f.text_area :description, class: 'form-control', placeholder: 'Add description...', style: 'width:500px' %> +
+ +
+ +
+ + <%= f.hidden_field :jira_base_url %> + <%= f.hidden_field :diff_id %> + +
+ <%= f.submit 'Create Jira', :class => 'btn btn-success' %> +
+<% end %> diff --git a/app/views/jiras/edit.html.erb b/app/views/jiras/edit.html.erb new file mode 100644 index 0000000..30ea62f --- /dev/null +++ b/app/views/jiras/edit.html.erb @@ -0,0 +1,5 @@ +<% breadcrumb :jira, @jira %> + +

Create Jira

+ +<%= render 'form' %> diff --git a/app/views/jiras/index.html.erb b/app/views/jiras/index.html.erb new file mode 100644 index 0000000..7c40b09 --- /dev/null +++ b/app/views/jiras/index.html.erb @@ -0,0 +1,26 @@ +<% breadcrumb :jiras %> +

Jiras

+ + + + + + + + + + + + + + <% @jiras.each do |jira| %> + + + + + + + + <% end %> + +
IDTitleComponentIssue TypePriority
<%= link_to jira.id, jira.jira_link %><%= link_to jira.title, jira.jira_link %><%= link_to jira.component, jira.jira_link %><%= link_to jira.issue_type, jira.jira_link %><%= link_to jira.priority, jira.jira_link %>
\ No newline at end of file diff --git a/app/views/jiras/index.json.jbuilder b/app/views/jiras/index.json.jbuilder new file mode 100644 index 0000000..589aac4 --- /dev/null +++ b/app/views/jiras/index.json.jbuilder @@ -0,0 +1,4 @@ +json.array!(@jiras) do |jira| + json.extract! jira, :id + json.url jira_url(jira, format: :json) +end diff --git a/app/views/jiras/new.html.erb b/app/views/jiras/new.html.erb new file mode 100644 index 0000000..92e989e --- /dev/null +++ b/app/views/jiras/new.html.erb @@ -0,0 +1,3 @@ +

New Jira

+ +<%= render 'form' %> diff --git a/app/views/jiras/show.html.erb b/app/views/jiras/show.html.erb new file mode 100644 index 0000000..5c27723 --- /dev/null +++ b/app/views/jiras/show.html.erb @@ -0,0 +1,4 @@ +

<%= link_to @jira.jira_key, @jira.jira_link %>

+ +<%= link_to 'Edit', edit_jira_path(@jira) %> | +<%= link_to 'Back', jiras_path %> diff --git a/app/views/jiras/show.json.jbuilder b/app/views/jiras/show.json.jbuilder new file mode 100644 index 0000000..5a2594e --- /dev/null +++ b/app/views/jiras/show.json.jbuilder @@ -0,0 +1 @@ +json.extract! @jira, :id, :created_at, :updated_at diff --git a/app/views/layouts/_base_images.erb b/app/views/layouts/_base_images.erb new file mode 100644 index 0000000..5c91c20 --- /dev/null +++ b/app/views/layouts/_base_images.erb @@ -0,0 +1,15 @@ + + +
+ <%= button_to base_images_test_images_project_path(@project), :class => 'btn-default btn pull-right glyphicon-spacing', :method => :get do %> + + <% end %> +
+ Base Images for <%= name %> +
+
+
+ <%= render_nested_tests(tests) %> +
+
+
\ No newline at end of file diff --git a/app/views/layouts/_diffs_panel.erb b/app/views/layouts/_diffs_panel.erb new file mode 100644 index 0000000..fb47c87 --- /dev/null +++ b/app/views/layouts/_diffs_panel.erb @@ -0,0 +1,58 @@ + + +
+
+ <%= title %> +
+
+
+ + + + + + + + + + + + + + + + + + + <% diffs.includes(:old_image, :new_image).find_each(batch_size: 200) do |diff| %> + + <% if diff.old_image && diff.new_image %> + + + + <% if diff.old_image.test && diff.new_image.test %> + + + <% end %> + + <% end %> + <% end %> + +
DiffOldNewTestInfo
<%= link_to image_tag(diff.differences.url(:thumbnail)), diff_path(diff) %><%= link_to image_tag(diff.old_image.image.url(:thumbnail)), diff_path(diff) %><%= link_to image_tag(diff.new_image.image.url(:thumbnail)), diff_path(diff) %><%= link_to diff.new_image.test.ancestry_key, test_path(diff.new_image.test) %> + <% if show_approver %> + Approved By: <%= link_to diff.approved_by_username, user_path(User.find_by_username(diff.approved_by_username)) %>
+ <% end %> + <% if diff.old_image.test.jira? %> + Jira: <%= link_to diff.old_image.test.jira, diff.old_image.test.jira %>
+ <% end %> + + <% if diff.old_image.test.pull_request_link? %> + Pull Request: <%= link_to diff.old_image.test.pull_request_link, diff.old_image.test.pull_request_link %>
+ <% end %> + <% if diff.old_image.test.comment? %> + Comment: <%= diff.old_image.test.comment %> + <% end %> +
+
+
+
diff --git a/app/views/layouts/_error_messages.erb b/app/views/layouts/_error_messages.erb new file mode 100644 index 0000000..59f158b --- /dev/null +++ b/app/views/layouts/_error_messages.erb @@ -0,0 +1,13 @@ +<% if object.errors.any? %> +
+
+ +

This form contains <%= pluralize(object.errors.count, 'error') %>.

+
    + <% object.errors.full_messages.each do |msg| %> +
  • <%= msg %>
  • + <% end %> +
+
+
+<% end %> \ No newline at end of file diff --git a/app/views/layouts/_test_images_panel.erb b/app/views/layouts/_test_images_panel.erb new file mode 100644 index 0000000..d42397e --- /dev/null +++ b/app/views/layouts/_test_images_panel.erb @@ -0,0 +1,48 @@ + + +
+
+ <%= title %> +
+
+
+ + + + + + + + + + + + + + + <% images.each do |test_image| %> + + <% if test_image&.image && test_image.test %> + + + + + <% end %> + +
ImageTestInfo
<%= link_to image_tag(test_image.image.url(:thumbnail)), test_image_path(test_image) %><%= link_to test_image.test_key, test_path(test_image.test) %> + <% end %> + <% if test_image.test.jira? %> + Jira: <%= link_to test_image.test.jira, test_image.test.jira %> + <% end %> +
+ <% if test_image.test.pull_request_link? %> + Pull Request: <%= link_to test_image.test.pull_request_link, test_image.test.pull_request_link %> + <% end %> +
+ <% if test_image.test.comment? %> + Comment: <%= test_image.test.comment %> + <% end %> +
+
+
+
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb new file mode 100644 index 0000000..32eeb94 --- /dev/null +++ b/app/views/layouts/application.html.erb @@ -0,0 +1,65 @@ + + + + Vizzy + <%= render 'application/favicon' %> + <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> + <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> + <%= csrf_meta_tags %> + + + + +
+
+ <%= flash_messages %> + <%= yield %> +
+ + diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb new file mode 100644 index 0000000..cbd34d2 --- /dev/null +++ b/app/views/layouts/mailer.html.erb @@ -0,0 +1,13 @@ + + + + + + + + + <%= yield %> + + diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000..37f0bdd --- /dev/null +++ b/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/app/views/projects/_form.html.erb b/app/views/projects/_form.html.erb new file mode 100644 index 0000000..41f4b70 --- /dev/null +++ b/app/views/projects/_form.html.erb @@ -0,0 +1,60 @@ +<%= form_for(@project) do |f| %> + <%= render 'layouts/error_messages', object: f.object %> +
+
+ + <%= f.text_field :name, class: 'form-control', placeholder: 'Add project name', style: 'width:500px' %> +
+
+ + <%= f.text_field :description, class: 'form-control', placeholder: 'Add project description', style: 'width:500px' %> +
+
+ + <%= f.text_field :github_root_url, class: 'form-control', placeholder: 'Add root url (e.g., https://github.com)', style: 'width:500px' %> +
+
+ + <%= f.text_field :github_repo, class: 'form-control', placeholder: 'Add repo name (e.g., "mobile/ios")', style: 'width:500px' %> +
+
+ + <%= f.text_field :github_status_context, class: 'form-control', placeholder: 'Add context to use for status updates (e.g., "continuous-integration/vizzy-tests")', style: 'width:500px' %> +
+ + <% PluginManager.instance.for_project(@project) %> + <% @project.plugin_settings.each do |plugin_settings| %> + <% plugin_key = plugin_settings.first %> + <% plugin_hash = plugin_settings.second %> +
+ <%= PluginManager.instance.get_plugin_name_from_unique_id(plugin_key) %> + +
+ <%= check_box_tag "enabled_plugins[#{plugin_key}]", true, plugin_hash[:enabled] %> +
+
+
+ <% plugin_hash.except(:enabled).each do |setting_details| %> + <% setting = setting_details.first %> + <% setting_hash = setting_details.second %> + <% plugin_prefix = "plugin_settings[#{plugin_key}][#{setting}]" %> + + + <%= text_field_tag "#{plugin_prefix}[value]", setting_hash[:value], class: 'form-control', placeholder: setting_hash[:placeholder], style: 'width:500px' %> + + + <%= hidden_field_tag "#{plugin_prefix}[display_name]", setting_hash[:display_name], class: 'form-control', style: 'width:500px' %> + <%= hidden_field_tag "#{plugin_prefix}[placeholder]", setting_hash[:placeholder], class: 'form-control', style: 'width:500px' %> +
+ <% end %> +
+
+
+ <% end %> +
+ <%= link_to 'Cancel', @project, class: 'btn btn-default' %> + <%= f.submit 'Submit', :class => 'btn btn-success' %> +
+
+
+<% end %> diff --git a/app/views/projects/base_images.erb b/app/views/projects/base_images.erb new file mode 100644 index 0000000..0db79bc --- /dev/null +++ b/app/views/projects/base_images.erb @@ -0,0 +1,10 @@ +<% breadcrumb :base_images, @project %> +<% if current_user.admin? %> + <%= button_to 'Delete All Base Images For Project', remove_all_base_images_project_path(@project), :class => 'btn-danger btn' %> +
+ <%= button_to 'Clean Base Images To Last Branch Build', clean_base_image_state_project_path(@project), :class => 'btn-danger btn' %> +
+<% end %> +<%= render partial: 'layouts/base_images', locals: {title: 'Base Images', + name: @project.name, + tests: @project.tests_ancestry_tree} %> \ No newline at end of file diff --git a/app/views/projects/base_images.json.builder b/app/views/projects/base_images.json.builder new file mode 100644 index 0000000..5deef82 --- /dev/null +++ b/app/views/projects/base_images.json.builder @@ -0,0 +1 @@ +json.extract! @project, :id, :name, :description, :created_at, :updated_at diff --git a/app/views/projects/base_images_test_images.erb b/app/views/projects/base_images_test_images.erb new file mode 100644 index 0000000..200e042 --- /dev/null +++ b/app/views/projects/base_images_test_images.erb @@ -0,0 +1,16 @@ +<% breadcrumb :base_images_test_images, @project %> + +
+
+ Base Images for <%= @project.name %> +
+
+ <% @project.calculate_base_images.each do |test_image| %> +
+
+ <%= link_to image_tag(test_image.image.url(:small)), test_image_path(test_image) %> +
+
+ <% end %> +
+
\ No newline at end of file diff --git a/app/views/projects/base_images_test_images.json.jbuilder b/app/views/projects/base_images_test_images.json.jbuilder new file mode 100644 index 0000000..f8da8e7 --- /dev/null +++ b/app/views/projects/base_images_test_images.json.jbuilder @@ -0,0 +1 @@ +json.extract! @project, :id, :name, :description, :created_at, :updated_at \ No newline at end of file diff --git a/app/views/projects/edit.html.erb b/app/views/projects/edit.html.erb new file mode 100644 index 0000000..88edb23 --- /dev/null +++ b/app/views/projects/edit.html.erb @@ -0,0 +1,4 @@ +<% breadcrumb :projects %> +

Edit Project

+ +<%= render 'form' %> diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb new file mode 100644 index 0000000..0125d33 --- /dev/null +++ b/app/views/projects/index.html.erb @@ -0,0 +1,50 @@ +<% breadcrumb :projects %> +

Projects

+ + + <% if current_user.admin? %> + + + + + + + + + <% else %> + + + + + <% end %> + + + + + + + <% if current_user.admin? %> + + + + <% end %> + + + + <% @projects.sort_by {|project| project[:id]}.each do |project| %> + + + + + + <% if current_user.admin? %> + + + + <% end %> + + <% end %> + +
IDNameDescriptionImagesEditDestroyCleanup
<%= link_to project.id, project %><%= link_to project.name, project %><%= link_to project.description, project %><%= link_to 'Base Images', base_images_project_path(project) %><%= link_to 'Edit Project', edit_project_path(project) %><%= link_to 'Destroy Project', project, method: :delete, data: {confirm: 'Are you sure?'} %><%= link_to 'Cleanup Uncommitted Builds', cleanup_uncommitted_builds_project_path(project), method: :post %>
+ +<%= link_to 'New Project', new_project_path, class: 'btn btn-primary' %> diff --git a/app/views/projects/index.json.jbuilder b/app/views/projects/index.json.jbuilder new file mode 100644 index 0000000..3ee87e9 --- /dev/null +++ b/app/views/projects/index.json.jbuilder @@ -0,0 +1,4 @@ +json.array!(@projects) do |project| + json.extract! project, :id, :name, :description + json.url project_url(project, format: :json) +end diff --git a/app/views/projects/new.html.erb b/app/views/projects/new.html.erb new file mode 100644 index 0000000..4f062b3 --- /dev/null +++ b/app/views/projects/new.html.erb @@ -0,0 +1,4 @@ +<% breadcrumb :projects %> +

New Project

+ +<%= render 'form' %> diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb new file mode 100644 index 0000000..f6377f2 --- /dev/null +++ b/app/views/projects/show.html.erb @@ -0,0 +1,102 @@ +<% breadcrumb :project, @project %> +
+ <%= button_to 'Current Base Images', base_images_project_path(@project), :class => 'btn-primary btn pull-right' %> +
+
+

<%= @project.name %> + + <%= @project.description %> + +

+
+<% pull_requests = @project.pull_requests %> +<% if pull_requests.size > 0 %> + <% column_div = "col-md-6" %> + +
+ + + + + + + + + + + + + + <% pull_requests.last(30).sort_by {|e| -e[:id]}.each do |build| %> + + + + + + <% end %> +
Latest Pull RequestsUserApproved
+ <%= link_to truncate(build.branch_name, length: 75), build_path(build) %> + <%= build.username %> + <% if build.unapproved_diffs.length == 0 && build.failure_message.blank? %> + <%= image_tag 'green_check.svg', :style => 'width: 22px; height: 22px;' %> + <% else %> + <%= image_tag 'red_x.svg', :style => 'width: 22px; height: 22px;' %> + <% end %> +
+
+<% else %> + <% column_div = "col-md-12" %> +<% end %> + +
> + + + + + + + + + + + + <% @project.branch_builds.last(10).sort_by {|e| -e[:id]}.each do |build| %> + + + + + <% end %> +
Latest BuildsApproved
+ <%= link_to truncate(build.title, length: 75), build_path(build) %> + + <% if build.unapproved_diffs.length == 0 && build.failure_message.blank? %> + <%= image_tag 'green_check.svg', :style => 'width: 22px; height: 22px;' %> + <% else %> + <%= image_tag 'red_x.svg', :style => 'width: 22px; height: 22px;' %> + <% end %> +
+
+ +
> + <% if @project.uncommitted_builds.size > 0 && current_user.admin? %> + + + + + + + + + + + + <% @project.uncommitted_builds.last(10).sort_by {|e| -e[:id]}.each do |build| %> + + + + <% end %> +
Uncommitted Builds<%= button_to 'Cleanup Old Uncommitted Builds', cleanup_uncommitted_builds_project_path(@project), :method => :post, :remote => true, :class => 'btn-primary btn pull-right' %>
+ <%= link_to truncate(build.title, length: 75), build_path(build) %> +
+ <% end %> +
\ No newline at end of file diff --git a/app/views/projects/show.json.jbuilder b/app/views/projects/show.json.jbuilder new file mode 100644 index 0000000..5deef82 --- /dev/null +++ b/app/views/projects/show.json.jbuilder @@ -0,0 +1 @@ +json.extract! @project, :id, :name, :description, :created_at, :updated_at diff --git a/app/views/test_images/index.html.erb b/app/views/test_images/index.html.erb new file mode 100644 index 0000000..5375c57 --- /dev/null +++ b/app/views/test_images/index.html.erb @@ -0,0 +1,34 @@ +<% breadcrumb :test_images %> +

Recent Test Images

+ + + + + + + + + + + + + + + <% @test_images.sort_by {|test_image| test_image.created_at}.reverse_each do |image| %> + + + + + + + + <% if image.branch.blank? %> + + <% else %> + + <% end %> + + + + <% end %> +
IDImageTest KeyBuild TitleApproved?MD5BranchBuild IDTest ID
<%= link_to image.id, test_image_path(image.id) %><%= image_tag image.image.url(:thumbnail) %><%= link_to image.test_key, test_image_path(image.id) %><%= link_to image.build.title, build_path(image.build.id) %><%= image.approved ? 'Yes' : 'No' %><%= link_to image.md5, test_image_path(image.id) %>-<%= link_to image.branch, test_image_path(image) %><%= link_to image.build.id, build_path(image.build.id) %><%= link_to image.test_id, test_path(image.test_id) %>
\ No newline at end of file diff --git a/app/views/test_images/index.json.jbuilder b/app/views/test_images/index.json.jbuilder new file mode 100644 index 0000000..6017206 --- /dev/null +++ b/app/views/test_images/index.json.jbuilder @@ -0,0 +1,3 @@ +json.array!(@test_images) do |test_image| + json.extract! test_image, :id, :test_id, :build_id, :approved, :test +end diff --git a/app/views/test_images/missing_tests.erb b/app/views/test_images/missing_tests.erb new file mode 100644 index 0000000..e342e5b --- /dev/null +++ b/app/views/test_images/missing_tests.erb @@ -0,0 +1,4 @@ +<% if @test_images.size > 0 %> + <% breadcrumb :missing_tests, @build %> + <%= render partial: 'layouts/test_images_panel', locals: {title: "#{pluralize(@test_images.size, 'missing test')}, here are the corresponding base images:", images: @test_images, panel_class: "panel-danger", panel_id: "missing_images"} %> +<% end %> \ No newline at end of file diff --git a/app/views/test_images/missing_tests.json.builder b/app/views/test_images/missing_tests.json.builder new file mode 100644 index 0000000..6017206 --- /dev/null +++ b/app/views/test_images/missing_tests.json.builder @@ -0,0 +1,3 @@ +json.array!(@test_images) do |test_image| + json.extract! test_image, :id, :test_id, :build_id, :approved, :test +end diff --git a/app/views/test_images/new.html.erb b/app/views/test_images/new.html.erb new file mode 100644 index 0000000..b5d542f --- /dev/null +++ b/app/views/test_images/new.html.erb @@ -0,0 +1,21 @@ +

New Image

+<%= form_for :test_image, :url => test_images_path, :html => {:multipart => true} do |form| %> +

+ <%= form.collection_select :build_id, Build.all, :id, :title, prompt: true %> + <%= form.label :build, 'Build number' %> +

+

+ <%= form.text_field :test_name %> + <%= form.label :test_name, 'Name of the test' %> +

+

+ <%= form.text_field :ancestry %> + <%= form.label :ancestry, 'Ancestry of the test' %> +

+

+ <%= form.file_field :image %> +

+

+ <%= form.submit %> +

+<% end %> \ No newline at end of file diff --git a/app/views/test_images/show.html.erb b/app/views/test_images/show.html.erb new file mode 100644 index 0000000..c36b162 --- /dev/null +++ b/app/views/test_images/show.html.erb @@ -0,0 +1,58 @@ +<% breadcrumb :test_image, @test_image %> +
+ <%= button_to 'Current Base Images', base_images_project_path(@test_image.build.project), :class => 'btn-primary btn pull-right' %> +
+
+
+
+
+

Test Image Details

+
+
+
+ Test Key: <%= link_to @test_image.test_key, test_path(@test_image.test) %> +
+ <% unless @test_image.build.title.blank? %> + Build: <%= link_to @test_image.build.title, build_path(@test_image.build.id) %> +
+ <% end %> + MD5: + <%= @test_image.md5 %> +
+ <% unless @test_image.branch.blank? %> + Branch: + <%= link_to @test_image.branch, test_image_path(@test_image) %> +
+ <% end %> + Build ID: + <%= link_to @test_image.build.id, build_path(@test_image.build.id) %> +
+ Test ID: + <%= link_to @test_image.test_id, test_path(@test_image.test_id) %> +
+ Approved? + <%= @test_image.approved ? 'Yes' : 'No' %> + <% @test_image.build.approved_diffs.each do |approved_diff| %> + <% if approved_diff.new_image.test_key == @test_image.test_key %> + <% unless approved_diff.approved_by_username.blank? %> +

+ + Approved By: + + <%= link_to "#{approved_diff.approved_by_username}", user_path(User.find_by_username(approved_diff.approved_by_username)) %> +

+ <% end %> + <% unless @test_image.build.is_branch_build %> +

+ Pull + Request: <%= link_to("#{@test_image.build.project.github_repo_url}/pull/#{@test_image.image_pull_request_number}", "#{@test_image.build.project.github_repo_url}/pull/#{@test_image.image_pull_request_number}") %> +
+

+ <% end %> + <% end %> + <% end %> +
+
+
+ +<%= image_tag @test_image.image.url(:small) %> \ No newline at end of file diff --git a/app/views/test_images/show.json.jbuilder b/app/views/test_images/show.json.jbuilder new file mode 100644 index 0000000..9850d88 --- /dev/null +++ b/app/views/test_images/show.json.jbuilder @@ -0,0 +1 @@ +json.extract! @test_image, :id, :test_id, :build_id, :approved, :test \ No newline at end of file diff --git a/app/views/test_images/successful_tests.erb b/app/views/test_images/successful_tests.erb new file mode 100644 index 0000000..28813e0 --- /dev/null +++ b/app/views/test_images/successful_tests.erb @@ -0,0 +1,4 @@ +<% if @test_images.size > 0 %> + <% breadcrumb :successful_tests, @build %> + <%= render partial: 'layouts/test_images_panel', locals: {title: "#{pluralize(@test_images.size, 'successful test')}, here are the corresponding base images:", images: @test_images, panel_class: "panel-success", panel_id: "successful_images"} %> +<% end %> \ No newline at end of file diff --git a/app/views/test_images/successful_tests.json.builder b/app/views/test_images/successful_tests.json.builder new file mode 100644 index 0000000..af748c7 --- /dev/null +++ b/app/views/test_images/successful_tests.json.builder @@ -0,0 +1,3 @@ +json.array!(@test_images) do |test_image| + json.extract! test_image, :id, :test_id, :build_id, :approved, :test +end \ No newline at end of file diff --git a/app/views/tests/_form.html.erb b/app/views/tests/_form.html.erb new file mode 100644 index 0000000..1896cbe --- /dev/null +++ b/app/views/tests/_form.html.erb @@ -0,0 +1,31 @@ +<%= form_for(@test) do |f| %> + <%= render 'layouts/error_messages', object: f.object %> + +
+
+
+ + <%= f.text_field :name, class: 'form-control', placeholder: 'Add test name', style: 'width:500px', readonly: 'true' %> +
+
+ + <%= f.text_field :description, class: 'form-control', placeholder: 'Add a description...', style: 'width:500px' %> +
+
+ + <%= f.url_field :jira, class: 'form-control', placeholder: 'Jira link only', style: 'width:500px' %> +
+
+ + <%= f.url_field :pull_request_link, class: 'form-control', placeholder: 'Pull request link only', style: 'width:500px' %> +
+
+ + <%= f.text_area :comment, class: 'form-control', rows:'3', placeholder: 'Add a comment...', style: 'width:500px' %> +
+
+ <%= link_to 'Cancel', @test, class: 'btn btn-default' %> + <%= f.submit 'Save', :class => 'btn btn-success' %> +
+
+<% end %> diff --git a/app/views/tests/edit.html.erb b/app/views/tests/edit.html.erb new file mode 100644 index 0000000..dce2d68 --- /dev/null +++ b/app/views/tests/edit.html.erb @@ -0,0 +1,5 @@ +<% breadcrumb :test, @test %> + +

Edit Test

+ +<%= render 'form' %> diff --git a/app/views/tests/index.html.erb b/app/views/tests/index.html.erb new file mode 100644 index 0000000..18db1b4 --- /dev/null +++ b/app/views/tests/index.html.erb @@ -0,0 +1,35 @@ +

Recent Tests

+ + + + + + + + + + + + + + + <% @tests.sort_by {|test| test.created_at}.reverse_each do |test| %> + + + + + + <% if test.jira.blank? %> + + <% else %> + + <% end %> + <% if test.comment.blank? %> + + <% else %> + + <% end %> + + <% end %> + +
IDNameAncestry KeyProject IDJira LinkComment
<%= link_to test.id, test_path(test.id) %> <%= link_to test.name, test_path(test.id) %> <%= link_to test.ancestry_key, test_path(test.id) %> <%= link_to test.project_id, projects_path(test.project_id) %> -<%= link_to test.jira, test.jira %> -<%= link_to test.comment, test_path(test.id) %>
\ No newline at end of file diff --git a/app/views/tests/index.json.jbuilder b/app/views/tests/index.json.jbuilder new file mode 100644 index 0000000..a46a5be --- /dev/null +++ b/app/views/tests/index.json.jbuilder @@ -0,0 +1,4 @@ +json.array!(@tests) do |test| + json.extract! test, :id, :name, :description, :test_suite_id + json.url test_url(test, format: :json) +end diff --git a/app/views/tests/new.html.erb b/app/views/tests/new.html.erb new file mode 100644 index 0000000..a29451b --- /dev/null +++ b/app/views/tests/new.html.erb @@ -0,0 +1,5 @@ +

New Test

+ +<%= render 'form' %> + +<%= link_to 'Back', tests_path %> diff --git a/app/views/tests/show.html.erb b/app/views/tests/show.html.erb new file mode 100644 index 0000000..7cc070c --- /dev/null +++ b/app/views/tests/show.html.erb @@ -0,0 +1,147 @@ +<% breadcrumb :test, @test %> +<%= button_to 'Current Base Images', base_images_project_path(@test.project_id), :class => 'btn-primary btn pull-right' %> +
+
+
+
+

Test Details

+
+
+
+ <%= button_to 'Delete All Base Images For Test', remove_base_images_test_path(@test), :class => 'btn-danger btn pull-right' %> + Name: <%= @test.name %> +
+ <% if @test.description? %> + Description: <%= @test.description %> +
+ <% end %> + <% if @test.pull_request_link? %> + Pull Request: + <%= link_to @test.pull_request_link, @test.pull_request_link %> +
+ <% end %> + <% if @test.jira? %> + Jira: + <%= link_to @test.jira, @test.jira %> +
+ <% end %> + <% if @test.comment? %> + Comment: + <%= @test.comment %> +
+ <% end %> + <% unless @test.parent.nil? %> + Parent Test: + <%= link_to @test.parent.name, test_path(@test.parent) %> +
+ <% end %> + <% unless @test.children.blank? %> + Children
+ <% @test.children.each do |child_test| %> + <%= link_to child_test.name, test_path(child_test) %>
+ <% end %> + <% end %> +
+
+
+ +<% test_images_history = @test.test_image_history %> +<% if test_images_history.size == 0 %> +
No test images associated with this test, please look at the children.
+<% end %> + +<% base_image = test_images_history.shift %> +<% unless base_image.nil? %> +
+
+
+ Current Base Image +
+
+
+
+ + + + + + + + + + + + + + + <% base_image = @test.current_base_image %> + <% build = Build.find(base_image.build_id) %> + + + + + + +
ImageAssociated BuildTime Of Upload
<%= link_to image_tag(base_image.image.url(:thumbnail)), test_image_path(base_image) %><%= link_to build.title, build_path(build) %> <%= base_image.image_updated_at.strftime("%b %d, %Y %I:%M:%S %P ") %>
+
+
+
+<% end %> + +<% if test_images_history.size > 0 %> +
+ +
+
+ History Of Approved Images For <%= @test.name %> +
+
+
+
+ + + + + + + + + + + + + + + <% test_images_history.each do |test_image| %> + <% build = Build.find(test_image.build_id) %> + + + + + + <% end %> + +
ImageAssociated BuildTime Of Upload
<%= link_to image_tag(test_image.image.url(:thumbnail)), test_image_path(test_image) %><%= link_to build.title, build_path(build) %> <%= test_image.image_updated_at.strftime("%b %d, %Y %I:%M:%S %P ") %>
+ +
+
+
+<% end %> diff --git a/app/views/tests/show.json.jbuilder b/app/views/tests/show.json.jbuilder new file mode 100644 index 0000000..5be9a6e --- /dev/null +++ b/app/views/tests/show.json.jbuilder @@ -0,0 +1 @@ +json.extract! @test, :id, :name, :description, :project_id, :created_at, :updated_at, :ancestry diff --git a/app/views/users/index.html.erb b/app/views/users/index.html.erb new file mode 100644 index 0000000..8377477 --- /dev/null +++ b/app/views/users/index.html.erb @@ -0,0 +1,22 @@ +<% breadcrumb :users %> +

Users

+ + + + + + + + + + + + <% @users.sort_by { |u| u.display_name.downcase }.each do |user| %> + + + + + + <% end %> + +
UsernameEmailRole
<%= link_to user.username, user_path(user) %><%= link_to user.email, user_path(user) %><%= link_to user.role, user_path(user) %>
\ No newline at end of file diff --git a/app/views/users/index.json.jbuilder b/app/views/users/index.json.jbuilder new file mode 100644 index 0000000..2a904f2 --- /dev/null +++ b/app/views/users/index.json.jbuilder @@ -0,0 +1,4 @@ +json.array!(@users) do |user| + json.extract! user, :id, :username, :role + json.url user_url(user, format: :json) +end diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb new file mode 100644 index 0000000..f5bf2a9 --- /dev/null +++ b/app/views/users/show.html.erb @@ -0,0 +1,32 @@ +<% breadcrumb :user, @user %> +

<%= @user.display_name %>

+
+
+

User Info

+
+
+ <% if current_user.admin? %> + <% if @user.role != :Owner %> + <%= button_to 'Delete', user_path(@user), :method => :delete, :remote => true, :class => 'btn-danger btn pull-right space-left', data: {:confirm => "Are you sure you want to delete #{@user.display_name}?"} %> + <% end %> + <% case @user.role %> + <% when :Admin %> + <%= button_to 'Remove Admin Rights', user_path(@user, :user => {:admin => false}), :method => :patch, :remote => true, :class => 'btn-danger btn pull-right' %> + <% when :Member %> + <%= button_to 'Grant Admin Rights', user_path(@user, :user => {:admin => true}), :method => :patch, :remote => true, :class => 'btn-success btn pull-right' %> + <% end %> + <% end %> + Email: <%= @user.email %> +
+ Role: <%= @user.role %> +
+ <% if @user == current_user %> +
+ <%= button_to 'Show Authentication Token', show_authentication_token_user_path(@user), :class => 'btn-primary btn' %> + <% end %> + <% if current_user.admin? %> +
+ <%= button_to 'Revoke Authentication Token', revoke_authentication_token_user_path(@user), :class => 'btn-danger btn' %> + <% end %> +
+
diff --git a/app/views/users/show.json.jbuilder b/app/views/users/show.json.jbuilder new file mode 100644 index 0000000..4e1f3d0 --- /dev/null +++ b/app/views/users/show.json.jbuilder @@ -0,0 +1 @@ +json.extract! @user, :id, :username, :email, :role diff --git a/bin/bundle b/bin/bundle new file mode 100755 index 0000000..66e9889 --- /dev/null +++ b/bin/bundle @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +load Gem.bin_path('bundler', 'bundle') diff --git a/bin/rails b/bin/rails new file mode 100755 index 0000000..0739660 --- /dev/null +++ b/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path('../config/application', __dir__) +require_relative '../config/boot' +require 'rails/commands' diff --git a/bin/rake b/bin/rake new file mode 100755 index 0000000..1724048 --- /dev/null +++ b/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative '../config/boot' +require 'rake' +Rake.application.run diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..e620b4d --- /dev/null +++ b/bin/setup @@ -0,0 +1,34 @@ +#!/usr/bin/env ruby +require 'pathname' +require 'fileutils' +include FileUtils + +# path to your application root. +APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +chdir APP_ROOT do + # This script is a starting point to setup your application. + # Add necessary setup steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') + + # puts "\n== Copying sample files ==" + # unless File.exist?('config/database.yml') + # cp 'config/database.yml.sample', 'config/database.yml' + # end + + puts "\n== Preparing database ==" + system! 'bin/rails db:setup' + + puts "\n== Removing old logs and tempfiles ==" + system! 'bin/rails log:clear tmp:clear' + + puts "\n== Restarting application server ==" + system! 'bin/rails restart' +end diff --git a/bin/spring b/bin/spring new file mode 100755 index 0000000..9bc076b --- /dev/null +++ b/bin/spring @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby + +# This file loads spring without using Bundler, in order to be fast. +# It gets overwritten when you run the `spring binstub` command. + +unless defined?(Spring) + require 'rubygems' + require 'bundler' + + lockfile = Bundler::LockfileParser.new(Bundler.default_lockfile.read) + if spring = lockfile.specs.detect { |spec| spec.name == "spring" } + Gem.use_paths Gem.dir, Bundler.bundle_path.to_s, *Gem.path + gem 'spring', spring.version + require 'spring/binstub' + end +end diff --git a/bin/update b/bin/update new file mode 100755 index 0000000..a8e4462 --- /dev/null +++ b/bin/update @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby +require 'pathname' +require 'fileutils' +include FileUtils + +# path to your application root. +APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +chdir APP_ROOT do + # This script is a way to update your development environment automatically. + # Add necessary update steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') + + puts "\n== Updating database ==" + system! 'bin/rails db:migrate' + + puts "\n== Removing old logs and tempfiles ==" + system! 'bin/rails log:clear tmp:clear' + + puts "\n== Restarting application server ==" + system! 'bin/rails restart' +end diff --git a/config.ru b/config.ru new file mode 100644 index 0000000..f7ba0b5 --- /dev/null +++ b/config.ru @@ -0,0 +1,5 @@ +# This file is used by Rack-based servers to start the application. + +require_relative 'config/environment' + +run Rails.application diff --git a/config/application.rb b/config/application.rb new file mode 100644 index 0000000..c234651 --- /dev/null +++ b/config/application.rb @@ -0,0 +1,23 @@ +require_relative 'boot' + +require 'rails/all' + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module VisualAutomation + class Application < Rails::Application + # Settings in config/environments/* take precedence over those specified here. + # Application configuration should go into files in config/initializers + # -- all .rb files in that directory are automatically loaded. + config.time_zone = 'Pacific Time (US & Canada)' + + # load vizzy server config + vizzy_config_path = Rails.root.join('config', 'vizzy.yaml') + unless File.exist?(vizzy_config_path) + raise "Missing vizzy.yaml configuration file" + end + config.vizzy = config_for(vizzy_config_path) + end +end diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 0000000..30f5120 --- /dev/null +++ b/config/boot.rb @@ -0,0 +1,3 @@ +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) + +require 'bundler/setup' # Set up gems listed in the Gemfile. diff --git a/config/breadcrumbs.rb b/config/breadcrumbs.rb new file mode 100644 index 0000000..b029c6f --- /dev/null +++ b/config/breadcrumbs.rb @@ -0,0 +1,82 @@ +crumb :projects do + link 'Projects', projects_path +end + +crumb :project do |project| + link project.name, project_path(project) + parent :projects +end + +crumb :builds do + link 'Builds', builds_path +end + +crumb :build do |build| + link build.title, build_path(build) + parent build.project +end + +crumb :diffs do + link 'Diffs', diffs_path +end + +crumb :diff do |diff| + link "Diff: #{diff.new_image.test_key}", diff_path(diff) + parent diff.build +end + +crumb :jiras do + link 'Jiras', jiras_path +end + +crumb :jira do |jira| + link 'Create Jira', jira_path(jira) + parent jira.diff +end + +crumb :test_images do + link 'Test Images', test_images_path +end + +crumb :test_image do |test_image| + link test_image.image_file_name, test_image_path(test_image) + parent test_image.build +end + +crumb :test do |test| + link test.name, test_path(test) + if test.parent.nil? + parent Project.find(test.project_id) + else + parent test.parent + end +end + +crumb :base_images do |project| + link 'Base Images', project_path(project) + parent project +end + +crumb :base_images_test_images do |project| + link 'Base Images Test Images', project_path(project) + parent project +end + +crumb :users do + link 'Users', users_path +end + +crumb :user do |user| + link user.display_name, user_path(user) + parent :users +end + +crumb :missing_tests do |build| + link 'Missing Tests', builds_path(build) + parent build +end + +crumb :successful_tests do |build| + link 'Successful Tests', builds_path(build) + parent build +end \ No newline at end of file diff --git a/config/cable.yml b/config/cable.yml new file mode 100644 index 0000000..88f201c --- /dev/null +++ b/config/cable.yml @@ -0,0 +1,12 @@ +development: + adapter: async + +ci_test: + adapter: async + +test: + adapter: async + +production: + adapter: redis + url: redis://localhost:6379/1 diff --git a/config/database.yml b/config/database.yml new file mode 100644 index 0000000..63a7e4e --- /dev/null +++ b/config/database.yml @@ -0,0 +1,30 @@ +default: &default + adapter: postgresql + encoding: utf8 + database: visualautomation_dev + pool: 5 + timeout: 5000 + +development: + <<: *default + database: visualautomation_development + +production: &production + adapter: postgresql + pool: <%= ENV["DB_POOL"] || ENV['RAILS_MAX_THREADS'] || 20 %> + timeout: 5000 + host: postgres + port: 5432 + username: <%= ENV['POSTGRES_USER'] %> + password: <%= ENV['POSTGRES_PASSWORD'] %> + encoding: utf8 + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. + +test: + <<: *default + +ci_test: + <<: *production \ No newline at end of file diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 0000000..426333b --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative 'application' + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/config/environments/ci_test.rb b/config/environments/ci_test.rb new file mode 100644 index 0000000..8e480e9 --- /dev/null +++ b/config/environments/ci_test.rb @@ -0,0 +1,98 @@ +VisualAutomation::Application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.cache_classes = true + + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true + + # Full error reports are disabled and caching is turned on. + config.consider_all_requests_local = false + config.action_controller.perform_caching = true + + # Disable serving static files from the `/public` folder by default since + # Apache or NGINX already handles this. + config.public_file_server.enabled = true + + # Compress JavaScripts and CSS. + config.assets.js_compressor = :uglifier + # config.assets.css_compressor = :sass + + # Do not fallback to assets pipeline if a precompiled asset is missed. + config.assets.compile = false + + # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.action_controller.asset_host = 'http://assets.example.com' + + # Specifies the header that your server uses for sending files. + # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache + # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX + + # Mount Action Cable outside main process or domain + # config.action_cable.mount_path = nil + # config.action_cable.url = 'wss://example.com/cable' + # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # config.force_ssl = true + + # Use the lowest log level to ensure availability of diagnostic information + # when problems arise. + config.log_level = :debug + + # Log rotating: The second arg is a number: the count of logs to retain at rotation -- in this case 5 files. + # The third arg is the number of bytes to put in the logs before rotating -- in this case 1000.megabytes, which means I will have at most 5GB of log files + # http://stackoverflow.com/questions/4883891/ruby-on-rails-production-log-rotation + num_logs_to_retain = 5 + config.logger = Logger.new(config.paths['log'].first, num_logs_to_retain, 1000.megabytes) + + # Prepend all log lines with the following tags. + config.log_tags = [ :request_id ] + + # Use a different cache store in CI. + # config.cache_store = :mem_cache_store + + # Use a real queuing backend for Active Job (and separate queues per environment) + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "visual_automation_#{Rails.env}" + config.action_mailer.perform_caching = false + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Send deprecation notices to registered listeners. + config.active_support.deprecation = :notify + + # Use default logging formatter so that PID and timestamp are not suppressed. + config.log_formatter = ::Logger::Formatter.new + + # Use a different logger for distributed setups. + # require 'syslog/logger' + # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') + + if ENV["RAILS_LOG_TO_STDOUT"].present? + logger = ActiveSupport::Logger.new(STDOUT) + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + end + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + Paperclip.options[:command_path] = '/usr/local/bin/' + + config.read_encrypted_secrets = true + + # ----- Custom Configurations ----- +end diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100644 index 0000000..24a628f --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,59 @@ +VisualAutomation::Application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # In the development environment your application's code is reloaded on + # every request. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.cache_classes = false + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable/disable caching. By default caching is disabled. + if Rails.root.join('tmp/caching-dev.txt').exist? + config.action_controller.perform_caching = true + + config.cache_store = :memory_store + config.public_file_server.headers = { + 'Cache-Control' => 'public, max-age=172800' + } + else + config.action_controller.perform_caching = false + + config.cache_store = :null_store + end + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false + config.action_mailer.perform_caching = false + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Debug mode disables concatenation and preprocessing of assets. + # This option may cause significant delays in view rendering with a large + # number of complex assets. + config.assets.debug = true + + # Suppress logger output for asset requests. + config.assets.quiet = true + + # Raises error for missing translations + # config.action_view.raise_on_missing_translations = true + + # Use an evented file watcher to asynchronously detect changes in source code, + # routes, locales, etc. This feature depends on the listen gem. + config.file_watcher = ActiveSupport::EventedFileUpdateChecker + + Paperclip.options[:command_path] = '/usr/local/bin/' + + config.read_encrypted_secrets = true + + # ----- Custom Configurations ----- +end diff --git a/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 0000000..80ca4c2 --- /dev/null +++ b/config/environments/production.rb @@ -0,0 +1,98 @@ +VisualAutomation::Application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.cache_classes = true + + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true + + # Full error reports are disabled and caching is turned on. + config.consider_all_requests_local = false + config.action_controller.perform_caching = true + + # Disable serving static files from the `/public` folder by default since + # Apache or NGINX already handles this. + config.public_file_server.enabled = true + + # Compress JavaScripts and CSS. + config.assets.js_compressor = :uglifier + # config.assets.css_compressor = :sass + + # Do not fallback to assets pipeline if a precompiled asset is missed. + config.assets.compile = false + + # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.action_controller.asset_host = 'http://assets.example.com' + + # Specifies the header that your server uses for sending files. + # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache + # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX + + # Mount Action Cable outside main process or domain + # config.action_cable.mount_path = nil + # config.action_cable.url = 'wss://example.com/cable' + # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # config.force_ssl = true + + # Use the lowest log level to ensure availability of diagnostic information + # when problems arise. + config.log_level = :debug + + # Log rotating: The second arg is a number: the count of logs to retain at rotation -- in this case 5 files. + # The third arg is the number of bytes to put in the logs before rotating -- in this case 1000.megabytes, which means I will have at most 5GB of log files + # http://stackoverflow.com/questions/4883891/ruby-on-rails-production-log-rotation + num_logs_to_retain = 5 + config.logger = Logger.new(config.paths['log'].first, num_logs_to_retain, 1000.megabytes) + + # Prepend all log lines with the following tags. + config.log_tags = [ :request_id ] + + # Use a different cache store in production. + # config.cache_store = :mem_cache_store + + # Use a real queuing backend for Active Job (and separate queues per environment) + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "visual_automation_#{Rails.env}" + config.action_mailer.perform_caching = false + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Send deprecation notices to registered listeners. + config.active_support.deprecation = :notify + + # Use default logging formatter so that PID and timestamp are not suppressed. + config.log_formatter = ::Logger::Formatter.new + + # Use a different logger for distributed setups. + # require 'syslog/logger' + # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') + + if ENV["RAILS_LOG_TO_STDOUT"].present? + logger = ActiveSupport::Logger.new(STDOUT) + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + end + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + Paperclip.options[:command_path] = '/usr/local/bin/' + + config.read_encrypted_secrets = true + + # ----- Custom Configurations ----- +end diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 0000000..6d309d1 --- /dev/null +++ b/config/environments/test.rb @@ -0,0 +1,46 @@ +VisualAutomation::Application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # The test environment is used exclusively to run your application's + # test suite. You never need to work with it otherwise. Remember that + # your test database is "scratch space" for the test suite and is wiped + # and recreated between test runs. Don't rely on the data there! + config.cache_classes = true + + # Do not eager load code on boot. This avoids loading your whole application + # just for the purpose of running a single test. If you are using a tool that + # preloads Rails for running tests, you may have to set it to true. + config.eager_load = false + + # Configure public file server for tests with Cache-Control for performance. + config.public_file_server.enabled = true + config.public_file_server.headers = { + 'Cache-Control' => 'public, max-age=3600' + } + + # Show full error reports and disable caching. + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + + # Raise exceptions instead of rendering exception templates. + config.action_dispatch.show_exceptions = false + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + config.action_mailer.perform_caching = false + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raises error for missing translations + # config.action_view.raise_on_missing_translations = true + + config.read_encrypted_secrets = true + + # ----- Custom Configurations ----- +end diff --git a/config/favicon.json b/config/favicon.json new file mode 100644 index 0000000..564e3c5 --- /dev/null +++ b/config/favicon.json @@ -0,0 +1,53 @@ +{ + "master_picture": "app/assets/images/vizzy-logo.svg", + "favicon_design": { + "ios": { + "picture_aspect": "no_change", + "assets": { + "ios6_and_prior_icons": false, + "ios7_and_later_icons": false, + "precomposed_icons": false, + "declare_only_default_icon": true + } + }, + "desktop_browser": [ + + ], + "windows": { + "picture_aspect": "no_change", + "background_color": "#da532c", + "on_conflict": "override", + "assets": { + "windows_80_ie_10_tile": false, + "windows_10_ie_11_edge_tiles": { + "small": false, + "medium": true, + "big": false, + "rectangle": false + } + } + }, + "android_chrome": { + "picture_aspect": "no_change", + "theme_color": "#ffffff", + "manifest": { + "display": "standalone", + "orientation": "not_set", + "on_conflict": "override", + "declared": true + }, + "assets": { + "legacy_icon": false, + "low_resolution_icons": false + } + }, + "safari_pinned_tab": { + "picture_aspect": "silhouette", + "theme_color": "#5bbad5" + } + }, + "settings": { + "scaling_algorithm": "Mitchell", + "error_on_image_too_small": false + } +} \ No newline at end of file diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb new file mode 100644 index 0000000..51639b6 --- /dev/null +++ b/config/initializers/application_controller_renderer.rb @@ -0,0 +1,6 @@ +# Be sure to restart your server when you modify this file. + +# ApplicationController.renderer.defaults.merge!( +# http_host: 'example.org', +# https: false +# ) diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb new file mode 100644 index 0000000..bb04222 --- /dev/null +++ b/config/initializers/assets.rb @@ -0,0 +1,11 @@ +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = '1.0' + +# Add additional assets to the asset load path +# Rails.application.config.assets.paths << Emoji.images_path + +# Precompile additional assets. +# application.js, application.css.scss, and all non-JS/CSS in app/assets folder are already added. +# Rails.application.config.assets.precompile += %w( search.js ) diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb new file mode 100644 index 0000000..59385cd --- /dev/null +++ b/config/initializers/backtrace_silencers.rb @@ -0,0 +1,7 @@ +# Be sure to restart your server when you modify this file. + +# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. +# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } + +# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. +# Rails.backtrace_cleaner.remove_silencers! diff --git a/config/initializers/bugsnag.rb b/config/initializers/bugsnag.rb new file mode 100644 index 0000000..85462bc --- /dev/null +++ b/config/initializers/bugsnag.rb @@ -0,0 +1,4 @@ +Bugsnag.configure do |config| + config.api_key = Rails.application.secrets.BUGSNAG_API_KEY + config.notify_release_stages = ['production'] +end diff --git a/config/initializers/commontator.rb b/config/initializers/commontator.rb new file mode 100644 index 0000000..1f1d423 --- /dev/null +++ b/config/initializers/commontator.rb @@ -0,0 +1,284 @@ +# Change the settings below to suit your needs +# All settings are initially set to their default values + +# Note: Do not "return" from a Proc, use "next" instead if necessary +Commontator.configure do |config| + # Engine Configuration + + # current_user_proc + # Type: Proc + # Arguments: the current controller (ActionController::Base) + # Returns: the current user (acts_as_commontator) + # The default works for Devise and similar authentication plugins + # Default: ->(controller) { controller.current_user } + config.current_user_proc = ->(controller) { controller.current_user } + + # javascript_proc + # Type: Proc + # Arguments: a view (ActionView::Base) + # Returns: a String that is appended to Commontator JS views + # Can be used, for example, to display/clear Rails error messages + # or to reapply JQuery UI styles after Ajax calls + # Objects visible in view templates can be accessed + # through the view object (for example, view.flash) + # However, the view does not include the main application's helpers + # Default: ->(view) { '$("#error_explanation").remove();' } + config.javascript_proc = ->(view) { '$("#error_explanation").remove();' } + + + + # User (acts_as_commontator) Configuration + + # user_name_proc + # Type: Proc + # Arguments: a user (acts_as_commontator) + # Returns: the user's name (String) + # Default: ->(user) { I18n.t('commontator.anonymous') } (all users are anonymous) + config.user_name_proc = lambda { |user| user.display_name } + + # user_link_proc + # Type: Proc + # Arguments: a user (acts_as_commontator), + # the app_routes (ActionDispatch::Routing::RoutesProxy) + # Returns: a path to the user's `show` page (String) + # If anything non-blank is returned, the user's name in comments + # comments will become a hyperlink pointing to this path + # The main application's routes can be accessed through the app_routes object + # Default: ->(user, app_routes) { '' } (no link) + config.user_link_proc = ->(user, app_routes) { '' } + + # user_avatar_proc + # Type: Proc + # Arguments: a user (acts_as_commontator), a view (ActionView::Base) + # Returns: a String containing a HTML tag pointing to the user's avatar image + # The commontator_gravatar_image_tag helper takes a user object, + # a border size and an options hash for Gravatar, and produces a Gravatar image tag + # See available options at http://en.gravatar.com/site/implement/images/) + # Note: Gravatar has several security implications for your users + # It makes your users trackable across different sites and + # allows de-anonymization attacks against their email addresses + # If you absolutely want to keep users' email addresses or identities secret, + # do not use Gravatar or similar services + # Default: ->(user, view) { + # view.commontator_gravatar_image_tag(user, 1, s: 60, d: 'mm') + # } + config.user_avatar_proc = ->(user, view) { + view.commontator_gravatar_image_tag(user, 1, s: 60, d: 'mm') + } + + # user_email_proc + # Type: Proc + # Arguments: a user (acts_as_commontator), a mailer (ActionMailer::Base) + # Returns: the user's email address (String) + # The default works for Devise's defaults + # If the mailer argument is nil, Commontator intends to hash the email and send the hash + # to Gravatar, so you should always return the user's email address (if using Gravatar) + # If the mailer argument is not nil, then Commontator intends to send an email to + # the address returned; you can prevent it from being sent by returning a blank String + # Default: ->(user, mailer) { user.try(:email) || '' } + config.user_email_proc = ->(user, mailer) { user.try(:email) || '' } + + + + # Thread/Commontable (acts_as_commontable) Configuration + + # comment_filter + # Type: Arel node (Arel::Nodes::Node) or nil + # Arel that filters visible comments + # If specified, visible comments will be filtered according to this Arel node + # A value of nil will cause no filtering to be done + # Moderators can manually override this filter for themselves + # Example: Commontator::Comment.arel_table[:deleted_at].eq(nil) (hides deleted comments) + # This is not recommended, as it can cause confusion over deleted comments + # If using pagination, it can also cause comments to change pages + # Default: nil (no filtering - all comments are visible) + config.comment_filter = nil + + # thread_read_proc + # Type: Proc + # Arguments: a thread (Commontator::Thread), a user (acts_as_commontator) + # Returns: a Boolean, true if and only if the user should be allowed to read that thread + # Note: can be called with a user object that is nil (if they are not logged in) + # Default: ->(thread, user) { true } (anyone can read any thread) + config.thread_read_proc = ->(thread, user) { true } + + # thread_moderator_proc + # Type: Proc + # Arguments: a thread (Commontator::Thread), a user (acts_as_commontator) + # Returns: a Boolean, true if and only if the user is a moderator for that thread + # If you want global moderators, make this proc true for them regardless of thread + # Default: ->(thread, user) { false } (no moderators) + config.thread_moderator_proc = ->(thread, user) { false } + + # comment_editing + # Type: Symbol + # Whether users can edit their own comments + # Valid options: + # :a (always) + # :l (only if it's the latest comment) + # :n (never) + # Default: :l + config.comment_editing = :l + + # comment_deletion + # Type: Symbol + # Whether users can delete their own comments + # Valid options: + # :a (always) + # :l (only if it's the latest comment) + # :n (never) + # Note: For moderators, see the next option + # Default: :l + config.comment_deletion = :l + + # moderator_permissions + # Type: Symbol + # What permissions moderators have + # Valid options: + # :e (delete and edit comments and close threads) + # :d (delete comments and close threads) + # :c (close threads only) + # Default: :d + config.moderator_permissions = :d + + # comment_voting + # Type: Symbol + # Whether users can vote on other users' comments + # Valid options: + # :n (no voting) + # :l (likes - requires acts_as_votable gem) + # :ld (likes/dislikes - requires acts_as_votable gem) + # Not yet implemented: + # :s (star ratings) + # :r (reputation system) + # Default: :n + config.comment_voting = :n + + # vote_count_proc + # Type: Proc + # Arguments: a thread (Commontator::Thread), pos (Fixnum), neg (Fixnum) + # Returns: vote count to be displayed (String) + # pos is the number of likes, or the rating, or the reputation + # neg is the number of dislikes, if applicable, or 0 otherwise + # Default: ->(thread, pos, neg) { "%+d" % (pos - neg) } + config.vote_count_proc = ->(thread, pos, neg) { "%+d" % (pos - neg) } + + # comment_order + # Type: Symbol + # What order to use for comments + # Valid options: + # :e (earliest comment first) + # :l (latest comment first) + # :ve (highest voted first; earliest first if tied) + # :vl (highest voted first; latest first if tied) + # Notes: + # :e is usually used in forums (discussions) + # :l is usually used in blogs (opinions) + # :ve and :vl are usually used where it makes sense to rate comments + # based on usefulness (q&a, reviews, guides, etc.) + # If :l is selected, the "reply to thread" form will appear before the comments + # Otherwise, it will appear after the comments + # Default: :e + config.comment_order = :e + + # new_comment_style + # Type: Symbol + # How to display the "new comment" form + # Valid options: + # :t (always present in the thread's page) + # :l (link to the form; opens in the same page using JS) + # Not yet implemented: + # :n (link to the form; opens in a new window) + # Default: :l + config.new_comment_style = :l + + # comments_per_page + # Type: Fixnum or nil + # Number of comments to display in each page + # Set to nil to disable pagination + # Any other value requires the will_paginate gem + # Default: nil (no pagination) + config.comments_per_page = nil + + # thread_subscription + # Type: Symbol + # Whether users can subscribe to threads to receive activity email notifications + # Valid options: + # :n (no subscriptions) + # :a (automatically subscribe when you comment; cannot do it manually) + # :m (manual subscriptions only) + # :b (both automatic, when commenting, and manual) + # Default: :n + config.thread_subscription = :n + + # email_from_proc + # Type: Proc + # Arguments: a thread (Commontator::Thread) + # Returns: the address emails are sent "from" (String) + # Important: If using subscriptions, change this to at least match your domain name + # Default: ->(thread) { + # "no-reply@#{Rails.application.class.parent.to_s.downcase}.com" + # } + config.email_from_proc = ->(thread) { + "no-reply@#{Rails.application.class.parent.to_s.downcase}.com" + } + + # commontable_name_proc + # Type: Proc + # Arguments: a thread (Commontator::Thread) + # Returns: a name that refers to the commontable object (String) + # If you have multiple commontable models, you can also pass this + # configuration value as an argument to acts_as_commontable for each one + # Default: ->(thread) { + # "#{thread.commontable.class.name} ##{thread.commontable.id}" } + config.commontable_name_proc = ->(thread) { + "#{thread.commontable.class.name} ##{thread.commontable.id}" } + + # comment_url_proc + # Type: Proc + # Arguments: a comment (Commontator::Comment), + # the app_routes (ActionDispatch::Routing::RoutesProxy) + # Returns: a String containing the url of the view that displays the given comment + # This usually is the commontable's "show" page + # The main application's routes can be accessed through the app_routes object + # Default: ->(comment, app_routes) { + # app_routes.polymorphic_url(comment.thread.commontable, anchor: "comment_#{comment.id}_div") + # } + # (defaults to the commontable's show url with an anchor pointing to the comment's div) + config.comment_url_proc = ->(comment, app_routes) { + app_routes.polymorphic_url(comment.thread.commontable, anchor: "comment_#{comment.id}_div") + } + + # mentions_enabled + # Type: Boolean + # Whether users can mention other users to subscribe them to the thread + # Valid options: + # false (no mentions) + # true (mentions enabled) + # Default: false + config.mentions_enabled = false + + # user_mentions_proc + # Type: Proc + # Arguments: + # the current user (acts_as_commontator) + # the current thread (Commontator::Thread) + # the search query inputted by user (String) + # Returns: an ActiveRecord Relation object + # Important notes: + # + # - The proc will be called internally with an empty search string. + # In that case, it MUST return all users that can be mentioned. + # + # - With mentions enabled, any registered user in your app is able + # to call this proc with any search query >= 3 characters. + # Make sure to handle SQL escaping properly and that the + # attribute being searched does not contain sensitive information. + # + # Default: ->(current_user, query) { + # current_user.class.where('username LIKE ?', "#{query}%") + # } + config.user_mentions_proc = ->(current_user, thread, query) { + current_user.class.where('username LIKE ?', "#{query}%") + } +end diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb new file mode 100644 index 0000000..17b7e4e --- /dev/null +++ b/config/initializers/cookies_serializer.rb @@ -0,0 +1,5 @@ +# Be sure to restart your server when you modify this file. + +# Specify a serializer for the signed and encrypted cookie jars. +# Valid options are :json, :marshal, and :hybrid. +Rails.application.config.action_dispatch.cookies_serializer = :json \ No newline at end of file diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb new file mode 100644 index 0000000..1587dd5 --- /dev/null +++ b/config/initializers/devise.rb @@ -0,0 +1,275 @@ +# Use this hook to configure devise mailer, warden hooks and so forth. +# Many of these configuration options can be set straight in your model. +Devise.setup do |config| + if VizzyConfig.instance.is_ldap_auth + config.warden do |manager| + manager.default_strategies(:scope => :user).unshift :ldap_authenticatable + end + end + # ==> Mailer Configuration + # Configure the e-mail address which will be shown in Devise::Mailer, + # note that it will be overwritten if you use your own mailer class + # with default "from" parameter. + config.mailer_sender = 'vizzy@gmail.com' + + # Configure the class responsible to send e-mails. + # config.mailer = 'Devise::Mailer' + + # Configure the parent class responsible to send e-mails. + # config.parent_mailer = 'ActionMailer::Base' + + # ==> ORM configuration + # Load and configure the ORM. Supports :active_record (default) and + # :mongoid (bson_ext recommended) by default. Other ORMs may be + # available as additional gems. + require 'devise/orm/active_record' + + # ==> Configuration for any authentication mechanism + # Configure which keys are used when authenticating a user. The default is + # just :email. You can configure it to use [:username, :subdomain], so for + # authenticating a user, both parameters are required. Remember that those + # parameters are used only when authenticating and not when retrieving from + # session. If you need permissions, you should implement that in a before filter. + # You can also supply a hash where the value is a boolean determining whether + # or not authentication should be aborted when the value is not present. + config.authentication_keys = [:email] + + # Configure parameters from the request object used for authentication. Each entry + # given should be a request method and it will automatically be passed to the + # find_for_authentication method and considered in your model lookup. For instance, + # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. + # The same considerations mentioned for authentication_keys also apply to request_keys. + # config.request_keys = [] + + # Configure which authentication keys should be case-insensitive. + # These keys will be downcased upon creating or modifying a user and when used + # to authenticate or find a user. Default is :email. + config.case_insensitive_keys = [:email] + + # Configure which authentication keys should have whitespace stripped. + # These keys will have whitespace before and after removed upon creating or + # modifying a user and when used to authenticate or find a user. Default is :email. + config.strip_whitespace_keys = [:email] + + # Tell if authentication through request.params is enabled. True by default. + # It can be set to an array that will enable params authentication only for the + # given strategies, for example, `config.params_authenticatable = [:database]` will + # enable it only for database (email + password) authentication. + # config.params_authenticatable = true + + # Tell if authentication through HTTP Auth is enabled. False by default. + # It can be set to an array that will enable http authentication only for the + # given strategies, for example, `config.http_authenticatable = [:database]` will + # enable it only for database authentication. The supported strategies are: + # :database = Support basic authentication with authentication key + password + # config.http_authenticatable = false + + # If 401 status code should be returned for AJAX requests. True by default. + # config.http_authenticatable_on_xhr = true + + # The realm used in Http Basic Authentication. 'Application' by default. + # config.http_authentication_realm = 'Application' + + # It will change confirmation, password recovery and other workflows + # to behave the same regardless if the e-mail provided was right or wrong. + # Does not affect registerable. + # config.paranoid = true + + # By default Devise will store the user in session. You can skip storage for + # particular strategies by setting this option. + # Notice that if you are skipping storage for all authentication paths, you + # may want to disable generating routes to Devise's sessions controller by + # passing skip: :sessions to `devise_for` in your config/routes.rb + config.skip_session_storage = [:http_auth] + + # By default, Devise cleans up the CSRF token on authentication to + # avoid CSRF token fixation attacks. This means that, when using AJAX + # requests for sign in and sign up, you need to get a new CSRF token + # from the server. You can disable this option at your own risk. + # config.clean_up_csrf_token_on_authentication = true + + # When false, Devise will not attempt to reload routes on eager load. + # This can reduce the time taken to boot the app but if your application + # requires the Devise mappings to be loaded during boot time the application + # won't boot properly. + # config.reload_routes = true + + # ==> Configuration for :database_authenticatable + # For bcrypt, this is the cost for hashing the password and defaults to 11. If + # using other algorithms, it sets how many times you want the password to be hashed. + # + # Limiting the stretches to just one in testing will increase the performance of + # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use + # a value less than 10 in other environments. Note that, for bcrypt (the default + # algorithm), the cost increases exponentially with the number of stretches (e.g. + # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). + config.stretches = Rails.env.test? ? 1 : 11 + + # Set up a pepper to generate the hashed password. + # config.pepper = '49582d592eff81dca5f887c3a52e64047500a58004c61dbb5f107a5d1eff9683c771b1a94f22c40c21305da719ead84ce4ac79c67641ef02235f162247554ffa' + + # Send a notification to the original email when the user's email is changed. + # config.send_email_changed_notification = false + + # Send a notification email when the user's password is changed. + # config.send_password_change_notification = false + + # ==> Configuration for :confirmable + # A period that the user is allowed to access the website even without + # confirming their account. For instance, if set to 2.days, the user will be + # able to access the website for two days without confirming their account, + # access will be blocked just in the third day. Default is 0.days, meaning + # the user cannot access the website without confirming their account. + # config.allow_unconfirmed_access_for = 2.days + + # A period that the user is allowed to confirm their account before their + # token becomes invalid. For example, if set to 3.days, the user can confirm + # their account within 3 days after the mail was sent, but on the fourth day + # their account can't be confirmed with the token any more. + # Default is nil, meaning there is no restriction on how long a user can take + # before confirming their account. + # config.confirm_within = 3.days + + # If true, requires any email changes to be confirmed (exactly the same way as + # initial account confirmation) to be applied. Requires additional unconfirmed_email + # db field (see migrations). Until confirmed, new email is stored in + # unconfirmed_email column, and copied to email column on successful confirmation. + config.reconfirmable = true + + # Defines which key will be used when confirming an account + # config.confirmation_keys = [:email] + + # ==> Configuration for :rememberable + # The time the user will be remembered without asking for credentials again. + # config.remember_for = 2.weeks + + # Invalidates all the remember me tokens when the user signs out. + config.expire_all_remember_me_on_sign_out = true + + # If true, extends the user's remember period when remembered via cookie. + # config.extend_remember_period = false + + # Options to be passed to the created cookie. For instance, you can set + # secure: true in order to force SSL only cookies. + # config.rememberable_options = {} + + # ==> Configuration for :validatable + # Range for password length. + config.password_length = 12..72 + + # Email regex used to validate email formats. It simply asserts that + # one (and only one) @ exists in the given string. This is mainly + # to give user feedback and not to assert the e-mail validity. + config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ + + # ==> Configuration for :timeoutable + # The time you want to timeout the user session without activity. After this + # time the user will be asked for credentials again. Default is 30 minutes. + # config.timeout_in = 30.minutes + + # ==> Configuration for :lockable + # Defines which strategy will be used to lock an account. + # :failed_attempts = Locks an account after a number of failed attempts to sign in. + # :none = No lock strategy. You should handle locking by yourself. + # config.lock_strategy = :failed_attempts + + # Defines which key will be used when locking and unlocking an account + # config.unlock_keys = [:email] + + # Defines which strategy will be used to unlock an account. + # :email = Sends an unlock link to the user email + # :time = Re-enables login after a certain amount of time (see :unlock_in below) + # :both = Enables both strategies + # :none = No unlock strategy. You should handle unlocking by yourself. + # config.unlock_strategy = :both + + # Number of authentication tries before locking an account if lock_strategy + # is failed attempts. + # config.maximum_attempts = 20 + + # Time interval to unlock the account if :time is enabled as unlock_strategy. + # config.unlock_in = 1.hour + + # Warn on the last attempt before the account is locked. + # config.last_attempt_warning = true + + # ==> Configuration for :recoverable + # + # Defines which key will be used when recovering the password for an account + # config.reset_password_keys = [:email] + + # Time interval you can reset your password with a reset password key. + # Don't put a too small interval or your users won't have the time to + # change their passwords. + config.reset_password_within = 6.hours + + # When set to false, does not sign a user in automatically after their password is + # reset. Defaults to true, so a user is signed in automatically after a reset. + # config.sign_in_after_reset_password = true + + # ==> Configuration for :encryptable + # Allow you to use another hashing or encryption algorithm besides bcrypt (default). + # You can use :sha1, :sha512 or algorithms from others authentication tools as + # :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20 + # for default behavior) and :restful_authentication_sha1 (then you should set + # stretches to 10, and copy REST_AUTH_SITE_KEY to pepper). + # + # Require the `devise-encryptable` gem when using anything other than bcrypt + # config.encryptor = :sha512 + + # ==> Scopes configuration + # Turn scoped views on. Before rendering "sessions/new", it will first check for + # "users/sessions/new". It's turned off by default because it's slower if you + # are using only default views. + # config.scoped_views = false + + # Configure the default scope given to Warden. By default it's the first + # devise role declared in your routes (usually :user). + # config.default_scope = :user + + # Set this configuration to false if you want /users/sign_out to sign out + # only the current scope. By default, Devise signs out all scopes. + # config.sign_out_all_scopes = true + + # ==> Navigation configuration + # Lists the formats that should be treated as navigational. Formats like + # :html, should redirect to the sign in page when the user does not have + # access, but formats like :xml or :json, should return 401. + # + # If you have any extra navigational formats, like :iphone or :mobile, you + # should add them to the navigational formats lists. + # + # The "*/*" below is required to match Internet Explorer requests. + # config.navigational_formats = ['*/*', :html] + + # The default HTTP method used to sign out a resource. Default is :delete. + config.sign_out_via = :delete + + # ==> OmniAuth + # Add a new OmniAuth provider. Check the wiki for more information on setting + # up on your models and hooks. + # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' + + # ==> Warden configuration + # If you want to use other strategies, that are not supported by Devise, or + # change the failure app, you can configure them inside the config.warden block. + # + # config.warden do |manager| + # manager.intercept_401 = false + # manager.default_strategies(scope: :user).unshift :some_external_strategy + # end + + # ==> Mountable engine configurations + # When using Devise inside an engine, let's call it `MyEngine`, and this engine + # is mountable, there are some extra configurations to be taken into account. + # The following options are available, assuming the engine is mounted as: + # + # mount MyEngine, at: '/my_engine' + # + # The router that invoked `devise_for`, in the example above, would be: + # config.router_name = :my_engine + # + # When using OmniAuth, Devise cannot automatically set OmniAuth path, + # so you need to do it manually. For the users scope, it would be: + # config.omniauth_path_prefix = '/my_engine/users/auth' +end diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000..1d2be06 --- /dev/null +++ b/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Configure sensitive parameters which will be filtered from the log file. +Rails.application.config.filter_parameters += [:password, :authentication_token] diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb new file mode 100644 index 0000000..ac033bf --- /dev/null +++ b/config/initializers/inflections.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, '\1en' +# inflect.singular /^(ox)en/i, '\1' +# inflect.irregular 'person', 'people' +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym 'RESTful' +# end diff --git a/config/initializers/ldap_authenticatable.rb b/config/initializers/ldap_authenticatable.rb new file mode 100644 index 0000000..b3dc8a7 --- /dev/null +++ b/config/initializers/ldap_authenticatable.rb @@ -0,0 +1,60 @@ +require 'net/ldap' +require 'devise/strategies/authenticatable' + +module Devise + module Strategies + class LdapAuthenticatable < Authenticatable + def authenticate! + if params[:user] + user = nil + auth_config = VizzyConfig.instance.get_config_value(['devise']) + if Rails.env.development? || Rails.env.test? || Rails.env.ci_test? + # No need to connect to ldap for development environment, just let them sign in + user = User.find_or_create_by(email: email) + elsif !password.blank? + # Use LDAP for production environment + ldap = Net::LDAP.new + ldap.host = auth_config['ldap_host'] + ldap.port = auth_config['ldap_port'] + ldap.base = auth_config['ldap_base'] + internal_domain = auth_config['ldap_email_internal_domain'] + ldap.auth username_from_email + internal_domain, password + + if ldap.host.blank? || ldap.port.blank? || ldap.base.blank? || internal_domain.blank? + raise "ldap auth configuration missing -- host: #{ldap.host}, port: #{ldap.port}, base: #{ldap.base}, internal_domain: #{internal_domain}" + end + + if ldap.bind + user = User.find_or_create_by(email: email) + else + fail(:invalid_login) + return + end + end + if user.nil? + fail(:invalid_login) + else + if user.new_record? + user.save! + end + success!(user) + end + end + end + + def username_from_email + email.split('@').first + end + + def email + params[:user][:email] + end + + def password + params[:user][:password] + end + end + end +end + +Warden::Strategies.add(:ldap_authenticatable, Devise::Strategies::LdapAuthenticatable) \ No newline at end of file diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb new file mode 100644 index 0000000..dc18996 --- /dev/null +++ b/config/initializers/mime_types.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Add new mime types for use in respond_to blocks: +# Mime::Type.register "text/richtext", :rtf diff --git a/config/initializers/new_framework_defaults.rb b/config/initializers/new_framework_defaults.rb new file mode 100644 index 0000000..a54e903 --- /dev/null +++ b/config/initializers/new_framework_defaults.rb @@ -0,0 +1,18 @@ +# Be sure to restart your server when you modify this file. +# +# This file contains migration options to ease your Rails 5.0 upgrade. +# +# Read the Rails 5.0 release notes for more info on each option. + +# Enable per-form CSRF tokens. Previous versions had false. +Rails.application.config.action_controller.per_form_csrf_tokens = true + +# Enable origin-checking CSRF mitigation. Previous versions had false. +Rails.application.config.action_controller.forgery_protection_origin_check = true + +# Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`. +# Previous versions had false. +ActiveSupport.to_time_preserves_timezone = true + +# Require `belongs_to` associations by default. Previous versions had false. +Rails.application.config.active_record.belongs_to_required_by_default = true \ No newline at end of file diff --git a/config/initializers/paperclip_media_type_spoof_detector_override.rb b/config/initializers/paperclip_media_type_spoof_detector_override.rb new file mode 100644 index 0000000..da88aa2 --- /dev/null +++ b/config/initializers/paperclip_media_type_spoof_detector_override.rb @@ -0,0 +1,8 @@ +require 'paperclip/media_type_spoof_detector' +module Paperclip + class MediaTypeSpoofDetector + def spoofed? + false + end + end +end \ No newline at end of file diff --git a/config/initializers/plugin.rb b/config/initializers/plugin.rb new file mode 100644 index 0000000..30365e4 --- /dev/null +++ b/config/initializers/plugin.rb @@ -0,0 +1,14 @@ +require 'find' + +def all_plugins + plugin_folder = Rails.root.join('app', 'plugins') + Find.find(plugin_folder).select { |path| path =~ /.*\.rb/ } +end + +def load_all_plugins + all_plugins.each do |plugin| + require plugin + end +end + +load_all_plugins \ No newline at end of file diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb new file mode 100644 index 0000000..84258d1 --- /dev/null +++ b/config/initializers/session_store.rb @@ -0,0 +1,3 @@ +# Be sure to restart your server when you modify this file. + +Rails.application.config.session_store :cookie_store, key: '_visual_automation_session' diff --git a/config/initializers/simple_token_authentication.rb b/config/initializers/simple_token_authentication.rb new file mode 100644 index 0000000..4fa5614 --- /dev/null +++ b/config/initializers/simple_token_authentication.rb @@ -0,0 +1,62 @@ +SimpleTokenAuthentication.configure do |config| + + # Configure the session persistence policy after a successful sign in, + # in other words, if the authentication token acts as a signin token. + # If true, user is stored in the session and the authentication token and + # email may be provided only once. + # If false, users must provide their authentication token and email at every request. + # config.sign_in_token = false + + # Configure the name of the HTTP headers watched for authentication. + # + # Default header names for a given token authenticatable entity follow the pattern: + # { entity: { authentication_token: 'X-Entity-Token', email: 'X-Entity-Email'} } + # + # When several token authenticatable models are defined, custom header names + # can be specified for none, any, or all of them. + # + # Note: when using the identifiers options, this option behaviour is modified. + # Please see the example below. + # + # Examples + # + # Given User and SuperAdmin are token authenticatable, + # When the following configuration is used: + # `config.header_names = { super_admin: { authentication_token: 'X-Admin-Auth-Token' } }` + # Then the token authentification handler for User watches the following headers: + # `X-User-Token, X-User-Email` + # And the token authentification handler for SuperAdmin watches the following headers: + # `X-Admin-Auth-Token, X-SuperAdmin-Email` + # + # When the identifiers option is set: + # `config.identifiers = { super_admin: :phone_number }` + # Then both the header names identifier key and default value are modified accordingly: + # `config.header_names = { super_admin: { phone_number: 'X-SuperAdmin-PhoneNumber' } }` + # + # Configure the name of the attribute used to identify the user for authentication. + # That attribute must exist in your model. + # + # The default identifiers follow the pattern: + # { entity: 'email' } + # + # Note: the identifer must match your Devise configuration, + # see https://github.com/plataformatec/devise/wiki/How-To:-Allow-users-to-sign-in-using-their-username-or-email-address#tell-devise-to-use-username-in-the-authentication_keys + # + # Note: setting this option does modify the header_names behaviour, + # see the header_names section above. + # + # Example: + # + # `config.identifiers = { super_admin: 'phone_number', user: 'uuid' }` + # + + # Configure the Devise trackable strategy integration. + # + # If true, tracking is disabled for token authentication: signing in through + # token authentication won't modify the Devise trackable statistics. + # + # If false, given Devise trackable is configured for the relevant model, + # then signing in through token authentication will be tracked as any other sign in. + # + # config.skip_devise_trackable = true +end \ No newline at end of file diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb new file mode 100644 index 0000000..cf733ef --- /dev/null +++ b/config/initializers/wrap_parameters.rb @@ -0,0 +1,14 @@ +# Be sure to restart your server when you modify this file. + +# This file contains settings for ActionController::ParamsWrapper which +# is enabled by default. + +# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. +ActiveSupport.on_load(:action_controller) do + wrap_parameters format: [:json] +end + +# To enable root element in JSON for ActiveRecord objects. +# ActiveSupport.on_load(:active_record) do +# self.include_root_in_json = true +# end diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml new file mode 100644 index 0000000..0b8f130 --- /dev/null +++ b/config/locales/devise.en.yml @@ -0,0 +1,64 @@ +# Additional translations at https://github.com/plataformatec/devise/wiki/I18n + +en: + devise: + confirmations: + confirmed: "Your email address has been successfully confirmed." + send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." + failure: + already_authenticated: "You are already signed in." + inactive: "Your account is not activated yet." + invalid: "Invalid %{authentication_keys} or password." + locked: "Your account is locked." + last_attempt: "You have one more attempt before your account is locked." + not_found_in_database: "Invalid %{authentication_keys} or password." + timeout: "Your session expired. Please sign in again to continue." + unauthenticated: "You need to sign in or sign up before continuing." + unconfirmed: "You have to confirm your email address before continuing." + mailer: + confirmation_instructions: + subject: "Confirmation instructions" + reset_password_instructions: + subject: "Reset password instructions" + unlock_instructions: + subject: "Unlock instructions" + email_changed: + subject: "Email Changed" + password_change: + subject: "Password Changed" + omniauth_callbacks: + failure: "Could not authenticate you from %{kind} because \"%{reason}\"." + success: "Successfully authenticated from %{kind} account." + passwords: + no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." + send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." + updated: "Your password has been changed successfully. You are now signed in." + updated_not_active: "Your password has been changed successfully." + registrations: + destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." + signed_up: "Welcome! You have signed up successfully." + signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." + signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." + signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." + update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirm link to confirm your new email address." + updated: "Your account has been updated successfully." + sessions: + signed_in: "Signed in successfully." + signed_out: "Signed out successfully." + already_signed_out: "Signed out successfully." + unlocks: + send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." + send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." + unlocked: "Your account has been unlocked successfully. Please sign in to continue." + errors: + messages: + already_confirmed: "was already confirmed, please try signing in" + confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" + expired: "has expired, please request a new one" + not_found: "not found" + not_locked: "was not locked" + not_saved: + one: "1 error prohibited this %{resource} from being saved:" + other: "%{count} errors prohibited this %{resource} from being saved:" diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100644 index 0000000..0653957 --- /dev/null +++ b/config/locales/en.yml @@ -0,0 +1,23 @@ +# Files in the config/locales directory are used for internationalization +# and are automatically loaded by Rails. If you want to use locales other +# than English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t 'hello' +# +# In views, this is aliased to just `t`: +# +# <%= t('hello') %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# To learn more, please read the Rails Internationalization guide +# available at http://guides.rubyonrails.org/i18n.html. + +en: + hello: "Hello world" diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 0000000..c7f311f --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,47 @@ +# Puma can serve each request in a thread from an internal thread pool. +# The `threads` method setting takes two numbers a minimum and maximum. +# Any libraries that use thread pools should be configured to match +# the maximum value specified for Puma. Default is set to 5 threads for minimum +# and maximum, this matches the default thread size of Active Record. +# +threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i +threads threads_count, threads_count + +# Specifies the `port` that Puma will listen on to receive requests, default is 3000. +# +port ENV.fetch("PORT") { 3000 } + +# Specifies the `environment` that Puma will run in. +# +environment ENV.fetch("RAILS_ENV") { "development" } + +# Specifies the number of `workers` to boot in clustered mode. +# Workers are forked webserver processes. If using threads and workers together +# the concurrency of the application would be max `threads` * `workers`. +# Workers do not work on JRuby or Windows (both of which do not support +# processes). +# +# workers ENV.fetch("WEB_CONCURRENCY") { 2 } + +# Use the `preload_app!` method when specifying a `workers` number. +# This directive tells Puma to first boot the application and load code +# before forking the application. This takes advantage of Copy On Write +# process behavior so workers use less memory. If you use this option +# you need to make sure to reconnect any threads in the `on_worker_boot` +# block. +# +# preload_app! + +# The code in the `on_worker_boot` will be called if you are using +# clustered mode by specifying a number of `workers`. After each worker +# process is booted this block will be run, if you are using `preload_app!` +# option you will want to use this block to reconnect to any threads +# or connections that may have been created at application boot, Ruby +# cannot share connections between processes. +# +# on_worker_boot do +# ActiveRecord::Base.establish_connection if defined?(ActiveRecord) +# end + +# Allow puma to be restarted by `rails restart` command. +plugin :tmp_restart diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000..599f391 --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,63 @@ +Rails.application.routes.draw do + # The priority is based upon order of creation: first created -> highest priority. + + devise_for :users + + root 'projects#index' + + resources :projects do + member do + post 'remove_all_base_images' + post 'clean_base_image_state' + post 'cleanup_uncommitted_builds' + post 'base_images' + get 'base_images' + get 'base_images_test_images' + end + end + + resources :builds do + member do + get 'unapproved_diffs' + get 'approved_diffs' + get 'new_tests' + get 'missing_tests' + get 'successful_tests' + post 'approve_all_images' + post 'add_md5s' + post 'commit' + get 'commit' + post 'fail' + end + end + + resources :diffs do + member do + post 'approve' + post 'unapprove' + post 'next' + post 'multiple_diff_approval' + post 'create_jira' + end + end + + resources :jiras + + resources :tests do + member do + post 'remove_base_images' + end + end + + resources :test_images + + resources :users do + member do + post 'show_authentication_token' + post 'revoke_authentication_token' + end + end + + # Commontator stuff + mount Commontator::Engine => '/commontator' +end diff --git a/config/schedule.rb b/config/schedule.rb new file mode 100644 index 0000000..92a06d5 --- /dev/null +++ b/config/schedule.rb @@ -0,0 +1,35 @@ +# Use this file to easily define all of your cron jobs. +# +# It's helpful, but not entirely necessary to understand cron before proceeding. +# http://en.wikipedia.org/wiki/Cron + +# Example: +# +# set :output, "/path/to/my/cron_log.log" +# +# every 2.hours do +# command "/usr/bin/some_great_command" +# runner "MyModel.some_method" +# rake "some:great:rake:task" +# end +# +# every 4.days do +# runner "AnotherModel.prune_old_records" +# end + +# Learn more: http://github.com/javan/whenever + +# Destroy all dev builds every day +every 1.day do + rake 'dev_builds:destroy', environment: 'production' +end + +# Destroy leaf pr builds every day +every 1.day do + rake 'orphaned_pr_builds:destroy', environment: 'production' +end + +# Destroy images that are not attached to a record every month +every 1.month do + rake 'paperclip:clean_orphan_files', environment: 'production' +end diff --git a/config/spring.rb b/config/spring.rb new file mode 100644 index 0000000..c9119b4 --- /dev/null +++ b/config/spring.rb @@ -0,0 +1,6 @@ +%w( + .ruby-version + .rbenv-vars + tmp/restart.txt + tmp/caching-dev.txt +).each { |path| Spring.watch(path) } diff --git a/config/vizzy.yaml b/config/vizzy.yaml new file mode 100644 index 0000000..2ec823f --- /dev/null +++ b/config/vizzy.yaml @@ -0,0 +1,39 @@ +defaults: &default + devise: + auth_strategy: 'local' + jira: + create_issues: yes + slack: + send_messages: yes + bamboo: + comment_on_build: yes + jenkins: + update_description: yes + +development: + <<: *default + +test: + <<: *default + jira: + create_issues: no + slack: + send_messages: no + bamboo: + comment_on_build: no + jenkins: + update_description: no + +ci_test: + <<: *default + jira: + create_issues: no + slack: + send_messages: no + bamboo: + comment_on_build: no + jenkins: + update_description: no + +production: + <<: *default \ No newline at end of file diff --git a/db/migrate/20180326171851_init_schema.rb b/db/migrate/20180326171851_init_schema.rb new file mode 100644 index 0000000..93627e5 --- /dev/null +++ b/db/migrate/20180326171851_init_schema.rb @@ -0,0 +1,177 @@ +class InitSchema < ActiveRecord::Migration[5.1] + def up + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" + create_table "builds", id: :serial, force: :cascade do |t| + t.string "url" + t.boolean "temporary", default: false + t.string "title" + t.datetime "created_at" + t.datetime "updated_at" + t.integer "project_id" + t.string "commit_sha" + t.string "pull_request_number" + t.integer "num_of_images_in_build", default: 0, null: false + t.text "image_md5s" + t.string "username", default: "Pull Request" + t.string "branch_name", default: "Branch" + t.text "associated_commit_shas" + t.text "full_list_of_image_md5s" + t.string "failure_message" + t.boolean "dev_build", default: false + t.index ["project_id"], name: "index_builds_on_project_id" + end + create_table "builds_base_images", id: :serial, force: :cascade do |t| + t.integer "build_id" + t.integer "test_image_id" + t.index ["build_id"], name: "index_builds_base_images_on_build_id" + t.index ["test_image_id"], name: "index_builds_base_images_on_test_image_id" + end + create_table "builds_successful_tests", id: :serial, force: :cascade do |t| + t.integer "build_id" + t.integer "test_image_id" + t.index ["build_id"], name: "index_builds_successful_tests_on_build_id" + t.index ["test_image_id"], name: "index_builds_successful_tests_on_test_image_id" + end + create_table "commontator_comments", id: :serial, force: :cascade do |t| + t.string "creator_type" + t.integer "creator_id" + t.string "editor_type" + t.integer "editor_id" + t.integer "thread_id", null: false + t.text "body", null: false + t.datetime "deleted_at" + t.integer "cached_votes_up", default: 0 + t.integer "cached_votes_down", default: 0 + t.datetime "created_at" + t.datetime "updated_at" + t.index ["cached_votes_down"], name: "index_commontator_comments_on_cached_votes_down" + t.index ["cached_votes_up"], name: "index_commontator_comments_on_cached_votes_up" + t.index ["creator_id", "creator_type", "thread_id"], name: "index_commontator_comments_on_c_id_and_c_type_and_t_id" + t.index ["thread_id", "created_at"], name: "index_commontator_comments_on_thread_id_and_created_at" + end + create_table "commontator_subscriptions", id: :serial, force: :cascade do |t| + t.string "subscriber_type", null: false + t.integer "subscriber_id", null: false + t.integer "thread_id", null: false + t.datetime "created_at" + t.datetime "updated_at" + t.index ["subscriber_id", "subscriber_type", "thread_id"], name: "index_commontator_subscriptions_on_s_id_and_s_type_and_t_id", unique: true + t.index ["thread_id"], name: "index_commontator_subscriptions_on_thread_id" + end + create_table "commontator_threads", id: :serial, force: :cascade do |t| + t.string "commontable_type" + t.integer "commontable_id" + t.datetime "closed_at" + t.string "closer_type" + t.integer "closer_id" + t.datetime "created_at" + t.datetime "updated_at" + t.index ["commontable_id", "commontable_type"], name: "index_commontator_threads_on_c_id_and_c_type", unique: true + end + create_table "diffs", id: :serial, force: :cascade do |t| + t.integer "old_image_id" + t.integer "new_image_id" + t.datetime "created_at" + t.datetime "updated_at" + t.string "differences_file_name" + t.string "differences_content_type" + t.integer "differences_file_size" + t.datetime "differences_updated_at" + t.integer "build_id" + t.integer "approved_by_id" + t.boolean "approved", default: false + t.index ["approved_by_id"], name: "index_diffs_on_approved_by_id" + t.index ["build_id"], name: "index_diffs_on_build_id" + t.index ["new_image_id"], name: "index_diffs_on_new_image_id" + t.index ["old_image_id"], name: "index_diffs_on_old_image_id" + end + create_table "jiras", id: :serial, force: :cascade do |t| + t.string "title" + t.string "project" + t.string "component" + t.string "description" + t.string "issue_type" + t.string "jira_link" + t.string "jira_key" + t.integer "diff_id" + t.string "assignee" + t.string "priority" + t.string "jira_base_url" + t.index ["diff_id"], name: "index_jiras_on_diff_id" + end + create_table "projects", id: :serial, force: :cascade do |t| + t.string "name" + t.string "description" + t.datetime "created_at" + t.datetime "updated_at" + t.string "github_root_url" + t.string "github_repo" + t.string "github_status_context", default: "continuous-integration/vizzy" + t.jsonb "plugin_settings", default: {}, null: false + t.string "vizzy_server_url" + t.index ["plugin_settings"], name: "index_projects_on_plugin_settings", using: :gin + end + create_table "test_images", id: :serial, force: :cascade do |t| + t.string "image_file_name" + t.string "image_content_type" + t.integer "image_file_size" + t.datetime "image_updated_at" + t.string "branch" + t.boolean "approved", default: false + t.datetime "created_at" + t.datetime "updated_at" + t.integer "build_id" + t.integer "test_id" + t.boolean "user_approved_this_build", default: false + t.boolean "image_created_this_build", default: false + t.string "image_pull_request_sha" + t.string "image_pull_request_number" + t.string "md5" + t.string "test_key" + t.index ["build_id"], name: "index_test_images_on_build_id" + t.index ["test_id"], name: "index_test_images_on_test_id" + end + create_table "tests", id: :serial, force: :cascade do |t| + t.string "name" + t.string "description" + t.datetime "created_at" + t.datetime "updated_at" + t.string "comment" + t.string "jira" + t.string "pull_request_link" + t.string "comment_user" + t.string "ancestry" + t.string "ancestry_key" + t.integer "project_id" + t.index ["ancestry"], name: "index_tests_on_ancestry" + t.index ["project_id"], name: "index_tests_on_project_id" + end + create_table "users", id: :serial, force: :cascade do |t| + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false + t.string "reset_password_token" + t.datetime "reset_password_sent_at" + t.datetime "remember_created_at" + t.integer "sign_in_count", default: 0, null: false + t.datetime "current_sign_in_at" + t.datetime "last_sign_in_at" + t.string "current_sign_in_ip" + t.string "last_sign_in_ip" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "admin", default: false + t.string "username" + t.string "authentication_token", limit: 30 + t.index ["authentication_token"], name: "index_users_on_authentication_token", unique: true + t.index ["email"], name: "index_users_on_email", unique: true + t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true + t.index ["username"], name: "index_users_on_username", unique: true + end + add_foreign_key "tests", "projects", on_delete: :cascade + end + + def down + raise ActiveRecord::IrreversibleMigration, "The initial migration is not revertable" + end +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 0000000..8bd408c --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,197 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# Note that this schema.rb definition is the authoritative source for your +# database schema. If you need to create the application database on another +# system, you should be using db:schema:load, not running all the migrations +# from scratch. The latter is a flawed and unsustainable approach (the more migrations +# you'll amass, the slower it'll run and the greater likelihood for issues). +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 20180326171851) do + + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" + + create_table "builds", id: :serial, force: :cascade do |t| + t.string "url" + t.boolean "temporary", default: false + t.string "title" + t.datetime "created_at" + t.datetime "updated_at" + t.integer "project_id" + t.string "commit_sha" + t.string "pull_request_number" + t.integer "num_of_images_in_build", default: 0, null: false + t.text "image_md5s" + t.string "username", default: "Pull Request" + t.string "branch_name", default: "Branch" + t.text "associated_commit_shas" + t.text "full_list_of_image_md5s" + t.string "failure_message" + t.boolean "dev_build", default: false + t.index ["project_id"], name: "index_builds_on_project_id" + end + + create_table "builds_base_images", id: :serial, force: :cascade do |t| + t.integer "build_id" + t.integer "test_image_id" + t.index ["build_id"], name: "index_builds_base_images_on_build_id" + t.index ["test_image_id"], name: "index_builds_base_images_on_test_image_id" + end + + create_table "builds_successful_tests", id: :serial, force: :cascade do |t| + t.integer "build_id" + t.integer "test_image_id" + t.index ["build_id"], name: "index_builds_successful_tests_on_build_id" + t.index ["test_image_id"], name: "index_builds_successful_tests_on_test_image_id" + end + + create_table "commontator_comments", id: :serial, force: :cascade do |t| + t.string "creator_type" + t.integer "creator_id" + t.string "editor_type" + t.integer "editor_id" + t.integer "thread_id", null: false + t.text "body", null: false + t.datetime "deleted_at" + t.integer "cached_votes_up", default: 0 + t.integer "cached_votes_down", default: 0 + t.datetime "created_at" + t.datetime "updated_at" + t.index ["cached_votes_down"], name: "index_commontator_comments_on_cached_votes_down" + t.index ["cached_votes_up"], name: "index_commontator_comments_on_cached_votes_up" + t.index ["creator_id", "creator_type", "thread_id"], name: "index_commontator_comments_on_c_id_and_c_type_and_t_id" + t.index ["thread_id", "created_at"], name: "index_commontator_comments_on_thread_id_and_created_at" + end + + create_table "commontator_subscriptions", id: :serial, force: :cascade do |t| + t.string "subscriber_type", null: false + t.integer "subscriber_id", null: false + t.integer "thread_id", null: false + t.datetime "created_at" + t.datetime "updated_at" + t.index ["subscriber_id", "subscriber_type", "thread_id"], name: "index_commontator_subscriptions_on_s_id_and_s_type_and_t_id", unique: true + t.index ["thread_id"], name: "index_commontator_subscriptions_on_thread_id" + end + + create_table "commontator_threads", id: :serial, force: :cascade do |t| + t.string "commontable_type" + t.integer "commontable_id" + t.datetime "closed_at" + t.string "closer_type" + t.integer "closer_id" + t.datetime "created_at" + t.datetime "updated_at" + t.index ["commontable_id", "commontable_type"], name: "index_commontator_threads_on_c_id_and_c_type", unique: true + end + + create_table "diffs", id: :serial, force: :cascade do |t| + t.integer "old_image_id" + t.integer "new_image_id" + t.datetime "created_at" + t.datetime "updated_at" + t.string "differences_file_name" + t.string "differences_content_type" + t.integer "differences_file_size" + t.datetime "differences_updated_at" + t.integer "build_id" + t.integer "approved_by_id" + t.boolean "approved", default: false + t.index ["approved_by_id"], name: "index_diffs_on_approved_by_id" + t.index ["build_id"], name: "index_diffs_on_build_id" + t.index ["new_image_id"], name: "index_diffs_on_new_image_id" + t.index ["old_image_id"], name: "index_diffs_on_old_image_id" + end + + create_table "jiras", id: :serial, force: :cascade do |t| + t.string "title" + t.string "project" + t.string "component" + t.string "description" + t.string "issue_type" + t.string "jira_link" + t.string "jira_key" + t.integer "diff_id" + t.string "assignee" + t.string "priority" + t.string "jira_base_url" + t.index ["diff_id"], name: "index_jiras_on_diff_id" + end + + create_table "projects", id: :serial, force: :cascade do |t| + t.string "name" + t.string "description" + t.datetime "created_at" + t.datetime "updated_at" + t.string "github_root_url" + t.string "github_repo" + t.string "github_status_context", default: "continuous-integration/vizzy" + t.jsonb "plugin_settings", default: {}, null: false + t.string "vizzy_server_url" + t.index ["plugin_settings"], name: "index_projects_on_plugin_settings", using: :gin + end + + create_table "test_images", id: :serial, force: :cascade do |t| + t.string "image_file_name" + t.string "image_content_type" + t.integer "image_file_size" + t.datetime "image_updated_at" + t.string "branch" + t.boolean "approved", default: false + t.datetime "created_at" + t.datetime "updated_at" + t.integer "build_id" + t.integer "test_id" + t.boolean "user_approved_this_build", default: false + t.boolean "image_created_this_build", default: false + t.string "image_pull_request_sha" + t.string "image_pull_request_number" + t.string "md5" + t.string "test_key" + t.index ["build_id"], name: "index_test_images_on_build_id" + t.index ["test_id"], name: "index_test_images_on_test_id" + end + + create_table "tests", id: :serial, force: :cascade do |t| + t.string "name" + t.string "description" + t.datetime "created_at" + t.datetime "updated_at" + t.string "comment" + t.string "jira" + t.string "pull_request_link" + t.string "comment_user" + t.string "ancestry" + t.string "ancestry_key" + t.integer "project_id" + t.index ["ancestry"], name: "index_tests_on_ancestry" + t.index ["project_id"], name: "index_tests_on_project_id" + end + + create_table "users", id: :serial, force: :cascade do |t| + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false + t.string "reset_password_token" + t.datetime "reset_password_sent_at" + t.datetime "remember_created_at" + t.integer "sign_in_count", default: 0, null: false + t.datetime "current_sign_in_at" + t.datetime "last_sign_in_at" + t.string "current_sign_in_ip" + t.string "last_sign_in_ip" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "admin", default: false + t.string "username" + t.string "authentication_token", limit: 30 + t.index ["authentication_token"], name: "index_users_on_authentication_token", unique: true + t.index ["email"], name: "index_users_on_email", unique: true + t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true + t.index ["username"], name: "index_users_on_username", unique: true + end + + add_foreign_key "tests", "projects", on_delete: :cascade +end diff --git a/db/seeds.rb b/db/seeds.rb new file mode 100644 index 0000000..c24d027 --- /dev/null +++ b/db/seeds.rb @@ -0,0 +1,7 @@ +# This file should contain all the record creation needed to seed the database with its default values. +# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). + +require 'factory_bot' + +FactoryBot.create(:project) +FactoryBot.create(:user) \ No newline at end of file diff --git a/k8s/deploy-vizzy.sh b/k8s/deploy-vizzy.sh new file mode 100755 index 0000000..82617cb --- /dev/null +++ b/k8s/deploy-vizzy.sh @@ -0,0 +1,107 @@ +#!/bin/sh -x + +while [ $# -gt 0 ]; do + case "$1" in + --api-server=*) + api_server="${1#*=}" + ;; + --bearer-token=*) + bearer_token="${1#*=}" + ;; + --rails-env=*) + rails_env="${1#*=}" + ;; + --vizzy-version=*) + vizzy_version="${1#*=}" + ;; + --namespace=*) + namespace="${1#*=}" + ;; + --vizzy-uri=*) + vizzy_uri="${1#*=}" + ;; + --replica-pods=*) + replica_pods="${1#*=}" + ;; + --memory=*) + memory="${1#*=}" + ;; + --docker-registry=*) + docker_registry="${1#*=}" + ;; + --run-tests=*) + run_tests="${1#*=}" + ;; + *) + printf "***************************\n" + printf "* Error: Invalid argument.*\n" + printf "***************************\n" + exit 1 + esac + shift +done + +export VIZZY_VERSION=$vizzy_version +export VIZZY_URI=$vizzy_uri +export VIZZY_REPLICA_PODS=$replica_pods +export VIZZY_REQUESTS_MEMORY=$memory +export VIZZY_LIMITS_MEMORY=$memory +export RAILS_ENV=$rails_env +export DOCKER_REGISTRY=$docker_registry + +kubectl config --kubeconfig=k8sconfig set-cluster k8s --server=$api_server +kubectl config --kubeconfig=k8sconfig set-credentials jenkins --token=$bearer_token +kubectl config --kubeconfig=k8sconfig set-context k8s --cluster=k8s --user=jenkins +kubectl config --kubeconfig=k8sconfig use-context k8s + +./render-template.sh kubernetes-vizzy-config.template.yaml > vizzy-config.yaml +./render-template.sh kubernetes-postgres-config.template.yaml > postgres-config.yaml + +kubectl --kubeconfig=k8sconfig --namespace=$namespace apply -f vizzy-pvc.yaml +kubectl --kubeconfig=k8sconfig --namespace=$namespace apply -f postgres-config.yaml +kubectl --kubeconfig=k8sconfig --namespace=$namespace apply -f vizzy-config.yaml + +rm vizzy-config.yaml +rm postgres-config.yaml +rm k8sconfig + +sleep 30 + +# Get the running visual pod and run the tests inside the pod +TEST_POD=$(kubectl get pods --namespace=$namespace | grep -m1 vizzy | awk '{print $1}') +echo ${TEST_POD} +kubectl exec ${TEST_POD} rake db:migrate --namespace=$namespace + +if [ $? -ne 0 ]; then + echo "Could not run migrations, pod does not exist. Exiting 1..." + exit 1 +fi + +if [ "$run_tests" = true ] ; then + echo 'Running unit tests...' + kubectl exec ${TEST_POD} rake test --namespace=$namespace + TEST_STATUS=$? + + if [ ${TEST_STATUS} -ne 0 ]; then + echo "Unit Tests Failed!" + # exit ${TEST_STATUS} + else + echo "Unit Tests Passed!" + fi + + # TODO: Enable system tests on CI + #kubectl exec ${TEST_POD} rails test:system --namespace=$namespace + #TEST_STATUS=$? + # + #if [ ${TEST_STATUS} -ne 0 ]; then + # echo "System Tests Failed!" + #else + # echo "System Tests Passed!" + #fi + + # Clean up deployment so nothing is left running + kubectl delete deployment vizzy --namespace=$namespace + kubectl delete deployment postgres --namespace=$namespace + + exit ${TEST_STATUS} +fi \ No newline at end of file diff --git a/k8s/kubernetes-postgres-config.template.yaml b/k8s/kubernetes-postgres-config.template.yaml new file mode 100644 index 0000000..52dfa77 --- /dev/null +++ b/k8s/kubernetes-postgres-config.template.yaml @@ -0,0 +1,71 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: postgres-storage +spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: 5000Mi + +--- + +apiVersion: v1 +kind: Service +metadata: + labels: + name: postgres + role: service + name: postgres +spec: + ports: + - port: 5432 + targetPort: 5432 + type: NodePort + selector: + name: postgres + +--- + +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: postgres +spec: + replicas: 1 + template: + metadata: + labels: + name: postgres + spec: + containers: + - name: postgres + image: ${DOCKER_REGISTRY}/visual/postgres:9.5.3 + resources: + requests: + memory: 300Mi + limits: + memory: 300Mi + ports: + - containerPort: 5432 + volumeMounts: + - name: postgres-storage + mountPath: /var/lib/postgresql/data + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: vizzy-postgres-secret + key: username + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: vizzy-postgres-secret + key: password + imagePullSecrets: + - name: docker-dev + volumes: + - name: postgres-storage + persistentVolumeClaim: + claimName: postgres-storage \ No newline at end of file diff --git a/k8s/kubernetes-vizzy-config.template.yaml b/k8s/kubernetes-vizzy-config.template.yaml new file mode 100644 index 0000000..801ee9a --- /dev/null +++ b/k8s/kubernetes-vizzy-config.template.yaml @@ -0,0 +1,82 @@ +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: vizzy +spec: + rules: + - host: ${VIZZY_URI} + http: + paths: + - backend: + serviceName: vizzy + servicePort: 3000 + +--- + +apiVersion: v1 +kind: Service +metadata: + labels: + name: vizzy + role: service + name: vizzy +spec: + ports: + - port: 3000 + targetPort: 3000 + selector: + name: vizzy + +--- + +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: vizzy +spec: + replicas: ${VIZZY_REPLICA_PODS} + template: + metadata: + labels: + name: vizzy + spec: + containers: + - name: visual + image: ${DOCKER_REGISTRY}/visual/vizzy:${VIZZY_VERSION} + resources: + requests: + memory: ${VIZZY_REQUESTS_MEMORY} + limits: + memory: ${VIZZY_LIMITS_MEMORY} + ports: + - name: web + containerPort: 3000 + volumeMounts: + - name: visual-data + mountPath: /app/public/visual_images + env: + - name: VIZZY_URI + value: ${VIZZY_URI} + - name: RAILS_ENV + value: ${RAILS_ENV} + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: vizzy-postgres-secret + key: username + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: vizzy-postgres-secret + key: password + - name: RAILS_MASTER_KEY + valueFrom: + secretKeyRef: + name: vizzy-rails-master-key-secret + key: token + imagePullSecrets: + - name: docker-dev + volumes: + - name: visual-data + persistentVolumeClaim: + claimName: visual-automation-home diff --git a/k8s/render-template.sh b/k8s/render-template.sh new file mode 100755 index 0000000..f30f86f --- /dev/null +++ b/k8s/render-template.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +render_template() { + eval "echo \"$(cat $1)\"" +} + +render_template $1 diff --git a/k8s/vizzy-pvc.yaml b/k8s/vizzy-pvc.yaml new file mode 100644 index 0000000..b7d8bb6 --- /dev/null +++ b/k8s/vizzy-pvc.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: visual-automation-home +spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: 10G \ No newline at end of file diff --git a/lib/assets/.keep b/lib/assets/.keep new file mode 100644 index 0000000..e69de29 diff --git a/lib/tasks/.keep b/lib/tasks/.keep new file mode 100644 index 0000000..e69de29 diff --git a/lib/tasks/dev_builds.rake b/lib/tasks/dev_builds.rake new file mode 100644 index 0000000..3556c2c --- /dev/null +++ b/lib/tasks/dev_builds.rake @@ -0,0 +1,14 @@ +desc 'Find dev builds and destroy them' +namespace :dev_builds do + task destroy: :environment do + puts 'Destroying dev builds older than 30 days...' + dev_builds = Build.where(dev_build: true).where('created_at < ?', 30.days.ago) + dev_build_count = dev_builds.size + if dev_build_count == 0 + puts 'No dev builds found, nothing to destroy.' + else + puts "Destroying #{dev_build_count} dev builds..." + dev_builds.destroy_all + end + end +end diff --git a/lib/tasks/fix_md5s.rake b/lib/tasks/fix_md5s.rake new file mode 100644 index 0000000..96cfc3d --- /dev/null +++ b/lib/tasks/fix_md5s.rake @@ -0,0 +1,24 @@ +desc 'Verify that the md5s in the database match the actual md5 of the image' +task :fix_md5s => :environment do + @dry_run = %w(true 1).include? ENV['DRY_RUN'] + + puts "Finding images with mismatched md5s" + images_updated = 0 + TestImage.find_each do |test_image| + file_md5 = Digest::MD5.file(test_image.image.path).hexdigest + db_md5 = test_image.md5 + if db_md5 != file_md5 + images_updated += 1 + if !@dry_run + test_image.md5 = file_md5 + test_image.save + end + end + end + if @dry_run + puts "Would have updated #{images_updated} md5s" + else + puts "Updated #{images_updated} md5s" + end +end + diff --git a/lib/tasks/invalid_images.rake b/lib/tasks/invalid_images.rake new file mode 100644 index 0000000..b514278 --- /dev/null +++ b/lib/tasks/invalid_images.rake @@ -0,0 +1,22 @@ +namespace :invalid_images do + desc 'Delete all invalid test images -- images that are not associated with a build' + task :destroy => :environment do + + @dry_run = %w(true 1).include? ENV['DRY_RUN'] + + puts "Starting invalid_images:destroy" + test_images = TestImage.where(build: nil).order("id DESC") + test_image_count = test_images.size + + if test_image_count == 0 + abort "No invalid images found. Exiting..." + end + + if @dry_run + puts "Invalid Images That Would Have Been Deleted: #{test_image_count}" + else + test_images.destroy_all + puts "Successfully Destroyed #{test_image_count} Invalid Test Images" + end + end +end diff --git a/lib/tasks/paperclip.rake b/lib/tasks/paperclip.rake new file mode 100644 index 0000000..3fae199 --- /dev/null +++ b/lib/tasks/paperclip.rake @@ -0,0 +1,79 @@ +namespace :paperclip do + + desc 'Destroy paperclip attachment files that are not attached to any record' + task :clean_orphan_files => :environment do + @last_path = nil + @dry_run = %w(true 1).include? ENV['DRY_RUN'] + + Signal.trap('USR1') do + puts "#{Time.now.strftime('%Y-%m-%d %H:%M:%S')} #{@last_path}" + end + + def reverse_id_partition(path) + parts = path.to_s.split('/')[-4..-2] + puts "Parts: #{parts}" + if parts.all? {|e| e =~ /^\d{3}$/} + parts.join.to_i + end + end + + def is_orphan?(model, id) + !model.exists?(id) + end + + def move_to_deleted_directory(old_path) + parts = old_path.to_s.split('/') + if parts.include?('images') + new_dir = old_path.to_s.gsub(/\bimages\b/, 'images_deleted') + new_path = Pathname.new(new_dir) + new_path.mkpath + + old_path.rename new_path + end + end + + def delete_dir_if_empty(dir) + if dir.children.none? {|e| (e.file? && e.extname != '') || e.directory?} + if @dry_run + puts "delete #{dir}" + else + dir.rmtree + end + end + end + + def move_dir_if_orphan(dir, model) + id = reverse_id_partition(dir) + puts "id: #{id} model: #{model}" + if id && is_orphan?(model, id) + if @dry_run + puts "#{model}##{id} : orphan" + else + puts "Moving #{model}##{id} : orphan" + move_to_deleted_directory(dir) + end + end + end + + def verify_directory(start_dir, model) + return unless start_dir.directory? + @last_path = start_dir.to_s + if start_dir.children.none? {|e| e.directory?} + move_dir_if_orphan(start_dir, model) + else + start_dir.children.sort.each do |entry| + full_path = (start_dir + entry) + if full_path.directory? + verify_directory(full_path, model) + end + end + delete_dir_if_empty(start_dir) + end + end + + start_dir = Pathname.new(TestImage::PAPERCLIP_BASEDIR + 'visual_images/test_images/images') + puts "start_dir: #{start_dir}" + verify_directory(start_dir, TestImage) + end + +end \ No newline at end of file diff --git a/lib/tasks/purge_pr_builds.rake b/lib/tasks/purge_pr_builds.rake new file mode 100644 index 0000000..c7fef3c --- /dev/null +++ b/lib/tasks/purge_pr_builds.rake @@ -0,0 +1,27 @@ +namespace :orphaned_pr_builds do + desc 'Delete builds that are orphaned, aka have no approved diffs (preapprovals) and no new tests (auto approved images)' + task :destroy => :environment do + @dry_run = %w(true 1).include? ENV['DRY_RUN'] + builds_destroyed = 0 + + non_temporary_pull_requests_older_than_30_days.find_each(batch_size: 500) do |build| + no_approved_diffs = build.diffs.where(approved: true).size == 0 + no_new_tests = build.new_tests.size == 0 + if no_approved_diffs && no_new_tests + puts "Deleting build with id: #{build.id}" + builds_destroyed += 1 + build.destroy unless @dry_run + end + end + + if @dry_run + puts "#{builds_destroyed} builds would have been destroyed" + else + puts "#{builds_destroyed} builds successfully destroyed" + end + end +end + +def non_temporary_pull_requests_older_than_30_days + Build.where.not(pull_request_number: -1).where(temporary: false).where('created_at < ?', 30.days.ago) +end \ No newline at end of file diff --git a/lib/tasks/rdoc.rake b/lib/tasks/rdoc.rake new file mode 100644 index 0000000..fa8e3cd --- /dev/null +++ b/lib/tasks/rdoc.rake @@ -0,0 +1,11 @@ +# Rakefile +require 'sdoc' # and use your RDoc task the same way you used it before +require 'rdoc/task' # ensure this file is also required in order to use `RDoc::Task` + +RDoc::Task.new do |rdoc| + rdoc.rdoc_dir = 'doc/rdoc' # name of output directory + rdoc.generator = 'sdoc' # explictly set the sdoc generator + rdoc.template = 'rails' # template used on api.rubyonrails.org + rdoc.main = 'README.md' + rdoc.rdoc_files.include('README.md', 'app/', 'lib/') +end diff --git a/lib/tasks/run_test_build.rake b/lib/tasks/run_test_build.rake new file mode 100644 index 0000000..231c6ab --- /dev/null +++ b/lib/tasks/run_test_build.rake @@ -0,0 +1,46 @@ +def run_build(params, args) + Dir.chdir Rails.root.join('test-image-upload') + build_command = "ruby run_test_push.rb #{params} #{args[:host_with_port]}" + git_sha = args[:git_sha] + build_command.concat(" #{git_sha}") unless git_sha.nil? + pull_request_number = args[:pull_request_number] + build_command.concat(" #{pull_request_number}") unless pull_request_number.nil? + system(build_command) +end + +namespace :run_test_build do +############## BUILD DEVELOPS ############## + + desc 'run a ruby script' + task :develop_1, [:host_with_port, :git_sha] => [:environment] do |_, args| + puts 'running develop 1...' + run_build('1 1', args) + end + + task :develop_2, [:host_with_port, :git_sha] => [:environment] do |_, args| + puts 'running develop 2...' + run_build('2 1', args) + end + + task :develop_3, [:host_with_port, :git_sha] => [:environment] do |_, args| + puts 'running develop 3...' + run_build('3 1', args) + end + + ############## PULL REQUESTS ############## + + task :pull_request_1, [:host_with_port, :git_sha, :pull_request_number] => [:environment] do |_, args| + puts 'running pull request 1...' + run_build('1 2', args) + end + + task :pull_request_2, [:host_with_port, :git_sha, :pull_request_number] => [:environment] do |_, args| + puts 'running pull request 2...' + run_build('2 2', args) + end + + task :pull_request_3, [:host_with_port, :git_sha, :pull_request_number] => [:environment] do |_, args| + puts 'running pull request 3...' + run_build('3 2', args) + end +end \ No newline at end of file diff --git a/lib/tasks/users.rake b/lib/tasks/users.rake new file mode 100644 index 0000000..1b74bcf --- /dev/null +++ b/lib/tasks/users.rake @@ -0,0 +1,19 @@ +namespace :users do + desc "Destory all user accounts" + task destroy_all: :environment do + User.destroy_all + end + + desc "Destroy a specific user account" + task :destroy_user, [:user_email] => [:environment] do |t, args| + user_email = args[:user_email].to_s + puts "Destroying user with email: #{user_email}" + user = User.find_by_email(user_email) + if user + puts "User found, destroying user account" + user.destroy + else + puts "User does not exist" + end + end +end \ No newline at end of file diff --git a/lib/tasks/wipeproject.rake b/lib/tasks/wipeproject.rake new file mode 100644 index 0000000..14c13c3 --- /dev/null +++ b/lib/tasks/wipeproject.rake @@ -0,0 +1,27 @@ +namespace :wipeproject do + desc 'Delete everything associated with the project id passed into the this rake task' + task :destroy, [:project_id] => [:environment] do |t, args| + + project_id = args[:project_id].to_i + @dry_run = %w(true 1).include? ENV['DRY_RUN'] + + puts "Starting wipeprojects:destroy with project id #{project_id}" + project = Project.find(project_id) + builds_count = project.builds.size + tests_count = project.tests.size + + if builds_count == 0 && tests_count == 0 + abort "Project with project id: #{project_id} has already been wiped clean. Exiting..." + end + + if @dry_run + puts "Builds That Would Have Been Deleted: #{builds_count}" + puts "Test That Would Have Been Deleted: #{tests_count}" + else + project.builds.order("id DESC").destroy_all + puts "Successfully Deleted #{builds_count} Builds From Project: #{project_id}" + project.tests.order("id DESC").destroy_all + puts "Successfully Deleted #{tests_count} Tests From Project: #{project_id}" + end + end +end diff --git a/log/development.log b/log/development.log new file mode 100644 index 0000000..c6188f3 --- /dev/null +++ b/log/development.log @@ -0,0 +1 @@ +No valid API key has been set, notifications will not be sent diff --git a/log/test.log b/log/test.log new file mode 100644 index 0000000..e69de29 diff --git a/public/404.html b/public/404.html new file mode 100644 index 0000000..b612547 --- /dev/null +++ b/public/404.html @@ -0,0 +1,67 @@ + + + + The page you were looking for doesn't exist (404) + + + + + + +
+
+

The page you were looking for doesn't exist.

+

You may have mistyped the address or the page may have moved.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/public/422.html b/public/422.html new file mode 100644 index 0000000..a21f82b --- /dev/null +++ b/public/422.html @@ -0,0 +1,67 @@ + + + + The change you wanted was rejected (422) + + + + + + +
+
+

The change you wanted was rejected.

+

Maybe you tried to change something you didn't have access to.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/public/500.html b/public/500.html new file mode 100644 index 0000000..061abc5 --- /dev/null +++ b/public/500.html @@ -0,0 +1,66 @@ + + + + We're sorry, but something went wrong (500) + + + + + + +
+
+

We're sorry, but something went wrong.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/public/apple-touch-icon-precomposed.png b/public/apple-touch-icon-precomposed.png new file mode 100644 index 0000000..e69de29 diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000..e69de29 diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..3c9c7c0 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,5 @@ +# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file +# +# To ban all spiders from the entire site uncomment the next two lines: +# User-agent: * +# Disallow: / diff --git a/public/upload_images_to_server.rb b/public/upload_images_to_server.rb new file mode 100755 index 0000000..3eeeca6 --- /dev/null +++ b/public/upload_images_to_server.rb @@ -0,0 +1,685 @@ +#!/usr/bin/ruby +# This script uploads images to the visual automation server. It calculates the image md5s, sends them with the build id request, the md5s are compared to the base image md5 on +# the server, generating a list of images that need to be uploaded that is returned with the build id. For usage of this script run with the --help flag. +require 'rubygems' +require 'json' +require 'digest/md5' +require 'net/http' +require 'net/https' +require 'net/http/post/multipart' +require 'benchmark' +require 'optparse' +require 'find' +require 'fileutils' +require 'pathname' + +# --------------- Server ---------------- # + +def create_server_client + url = URI(@server_url) + http = Net::HTTP.new(url.host, url.port) + if @server_url.start_with?('https') + http.use_ssl = true + http.verify_mode = OpenSSL::SSL::VERIFY_PEER + http.read_timeout = 300 + end + http +end + +def setup_shared_server_client + @http = create_server_client +end + +def setup_auth_creds(options) + @vizzy_auth_email = options.user_email + @vizzy_auth_token = options.user_token +end + +def print_vizzy_link(build_id = nil) + vizzy_build_link = @server_url + vizzy_build_link += "/builds/#{build_id}" unless build_id.nil? + puts "Vizzy Link: #{vizzy_build_link}" +end + +def do_post(uri, body) + perform_request(uri, body, false) +end + +def do_get(uri) + perform_request(uri, nil, true) +end + +def add_vizzy_auth_headers(request) + request.add_field 'X-User-Email', @vizzy_auth_email + request.add_field 'X-User-Token', @vizzy_auth_token +end + +def perform_request(uri, body, is_get) + request = is_get ? Net::HTTP::Get.new(uri) : Net::HTTP::Post.new(uri) + request.add_field 'Content-Type', 'application/json' + request.add_field 'Accept', 'application/json' + add_vizzy_auth_headers(request) + request.body = JSON.dump(body) unless body.nil? + fetch_request(request) +end + +HTTP_RETRY_ERRORS = [ + Net::ReadTimeout, + Net::HTTPBadGateway, + Errno::ECONNRESET, + Errno::EPIPE, + Errno::ETIMEDOUT, + Net::HTTPServiceUnavailable, +] + +# Fetch Request And Handle Errors +def fetch_request(request) + max_retries = 3 + times_retried = 0 + + begin + response = @http.request(request) + puts "Response HTTP Status Code: #{response.code}" + puts "Response HTTP Response Body: #{response.body}" + response + rescue *HTTP_RETRY_ERRORS => e + if times_retried < max_retries + times_retried += 1 + puts "Error: #{e}, retry #{times_retried}/#{max_retries}" + retry + else + puts "Error: #{e}, HTTP Request Failed: #{e.message}" + end + rescue StandardError => e + puts "HTTP Request failed (#{e.message})" + end +end + +# --------------- Actions --------------- # + +def fail_build(message, build_id = nil) + puts "Build was not successful. Error: #{message}" + unless build_id.nil? + print_vizzy_link(build_id) + body = { failure_message: message } + do_post("/builds/#{build_id}/fail.json", body) + end + exit(1) +end + +# Request (POST) +# Upload file to test_images controller with build id provided +def push_image_to_server(file_path, ancestry, build_id, thread_id, http) + max_retries = 3 + times_retried = 0 + + thread_puts = lambda do |msg| + puts "||#{thread_id}|| #{msg}" + end + + begin + time = Benchmark.realtime do + File.open(File.expand_path(file_path)) do |png| + request = Net::HTTP::Post::Multipart.new '/test_images.json', + image: UploadIO.new(png, 'image/png'), + build_id: build_id, + test_image_ancestry: ancestry + + add_vizzy_auth_headers(request) + response = http.request(request) + + thread_puts.call "Response HTTP Status Code: #{response.code}" + thread_puts.call "Response HTTP Response Body: #{response.body}" + + if response.code == '502' + # retry upload when we see a 502 server gateway error. Treat it as a read timeout. + raise Net::ReadTimeout + end + response + end + end + thread_puts.call "Time Spent: #{time}" + rescue *HTTP_RETRY_ERRORS => e + if times_retried < max_retries + times_retried += 1 + thread_puts.call "Error: #{e}, retry #{times_retried}/#{max_retries}" + retry + else + thread_puts.call "Error: #{e}, HTTP Request Failed: #{e.message}" + end + rescue StandardError => e + thread_puts.call "HTTP Request failed (#{e.message})" + end +end + +# Request (POST) +# Gets build id and images to upload +def get_build_id(create_options) + puts 'Requesting build id...' + body = { + title: create_options.title, + project_id: create_options.project, + commit_sha: create_options.commit, + pull_request_number: create_options.pull_request_number, + url: create_options.url, + dev_build: create_options.dev_build + } + do_post('/builds.json', body) +end + +# Request (POST) +# Sends a request to upload the generated md5s +def send_image_md5s(build_id, image_md5s) + puts 'Sending md5s for the build' + body = { image_md5s: image_md5s } + response = do_post("/builds/#{build_id}/add_md5s.json", body) + JSON.parse(response.body) +end + +# Request (POST) +# Sends a request, finalizing the build transaction +def commit_build(build_id, total_upload_count) + puts 'Committing the build' + commit_response = do_post("/builds/#{build_id}/commit.json", nil) + if commit_response.code != '200' + uncommited_json = JSON.parse(commit_response.body) + fail_build(uncommited_json['error'], build_id) + else + poll_for_commit(build_id, total_upload_count) + end +end + +def poll_for_commit(build_id, images_uploaded) + puts 'Waiting for commit to finalize' + sleep 2 # Sleep 2 before the first check so we can catch the 'simple' commits that have no diffs quickly + + # floor: 60 tries * 10 seconds == 10 minutes + max_tries = 60 + # scaling as needed for when there are more than 60 images, 10 seconds per image + max_tries = images_uploaded if images_uploaded > max_tries + # ceiling 360 * 10 seconds = 3600 seconds = 1 hour + max_tries = 360 if images_uploaded > 360 + + time_in_seconds = max_tries * 10 + time_in_minutes = time_in_seconds / 60 + puts "Polling Timeout Horizon: #{time_in_seconds} seconds == #{time_in_minutes} minutes" + + wait_time = 10 + total_wait_time = 0 + max_tries.times do + response = do_get("/builds/#{build_id}/commit.json") + if response.code != '200' + fail_build('Failed to poll for commit status.', build_id) + return nil + else + result = JSON.parse(response.body) + return result if result['committed'] + puts "Build not committed, checking again in #{wait_time} seconds. Have already waited #{total_wait_time} seconds." + total_wait_time += wait_time + sleep wait_time + end + end + fail_build("Build not committed after #{total_wait_time} seconds.", build_id) +end + +def upload_images_to_build_with_id(id, images_to_upload, test_image_folder) + return if images_to_upload.nil? + total_upload_count = images_to_upload.size + number_of_images_uploaded = 0 + file_list = traverse_directories(test_image_folder) + file_list.select! { |file_info| images_to_upload.key?(file_info[:ancestry]) } + + threads = [] + semaphore = Mutex.new + 3.times do |thread_id| + thread = Thread.new do + # Create a separate http object for each thread so they don't clobber each others requests + http = create_server_client + loop do + file_info = nil + semaphore.synchronize do # Syncronize on the file list / image count + next if file_list.empty? + file_info = file_list.pop + number_of_images_uploaded += 1 + puts "||#{thread_id}|| Uploading #{number_of_images_uploaded}/#{total_upload_count}: #{file_info[:file]}" + end + break if file_info.nil? + push_image_to_server(file_info[:file], file_info[:ancestry], id, thread_id, http) + end + end + threads.push(thread) + end + threads.each(&:join) +end + +# Returns a hash containing each image name and the associated md5 for that image, as json +def compute_image_md5s_json(test_image_folder) + image_md5s = {} + traverse_directories(test_image_folder) do |ancestry, file| + md5 = `md5 -q #{file}` + md5 = md5.tr("\n", '').to_s + image_md5s[ancestry] = md5 + end + { count: image_md5s.size, json: image_md5s.to_json } +end + +def traverse_directories(test_image_folder) + results = [] + png_file_paths = get_png_file_paths(test_image_folder) + + Dir.chdir(test_image_folder) do + test_image_folder_path = Dir.pwd + png_file_paths.each do |file_path| + + file_path = file_path.gsub(test_image_folder, '')[1..-1] + + absolute_file_path = file_path + unless Pathname.new(file_path).absolute? + absolute_file_path = "#{test_image_folder_path}/#{file_path}" + end + + filename = File.basename(absolute_file_path) + + if has_special_characters(filename) + new_file_name = remove_special_characters(filename) + new_absolute_file_path = get_new_file_path(absolute_file_path, new_file_name) + absolute_file_path = rename_file(absolute_file_path, new_absolute_file_path) + end + + ancestry_file_path = absolute_file_path.gsub(test_image_folder_path, '')[1..-1] + ancestry = ancestry_file_path.rpartition('.').first + + if block_given? + yield ancestry, absolute_file_path + else + results.push(ancestry: ancestry, file: absolute_file_path) + end + end + end + results unless block_given? +end + +def get_new_file_path(absolute_file_path, new_file_name) + path_array = absolute_file_path.split('/') + path_array.pop + path_array.push(new_file_name) + path_array.join('/') +end + +def rename_file(file_path, new_file_path) + puts "Renaming file from: #{file_path} -> #{new_file_path}" + File.rename(file_path, new_file_path) + new_file_path +end + +def get_png_file_paths(test_image_folder) + Find.find(test_image_folder).select { |path| get_png_paths(path) } +end + +def get_png_paths(path) + path =~ /.*\.png/ +end + +# Remove special characters that do not play nicely with the rails filesystem +# U+0000 (NUL) +# / (slash) +# \ (backslash) +#: (colon) +# * (asterisk) +# ? (question mark) +# " (quote) +# < (less than) +# @ (At symbol) +# >(greater than) +# | (pipe) +@special_character_regexp = /[\x00\/\\:\*\?\"@<>\|]/ + +def remove_special_characters(filename) + filename.gsub(@special_character_regexp, '_') +end + +def has_special_characters(filename) + filename =~ @special_character_regexp +end + +# Traverses the folder structure and returns the number of images +def get_test_images_count(folder) + Dir[File.join(folder, '**', '*')].count { |file| File.file?(file) } +end + +# -------------- Script Start -------------- # + +def validate_arguments(parser, options) + unless block_given? + puts 'Method requires block for required parameters' + exit(1) + end + + server_url = nil + parser.order!(ARGV) do |arg| + unless server_url.nil? + puts 'Too many arguments!' + puts parser + exit(1) + end + server_url = arg + end + + if server_url.nil? + puts 'Missing server_url!' + puts parser + exit(1) + end + + required_options = yield options + + required_options.each do |opt| + next unless options[opt].nil? + puts "Missing required option '--#{opt}'!" + puts parser + exit(1) + end + + setup_auth_creds(options) + @server_url = server_url +end + +# Add auth and token to opts. Used for create, upload, and fail +def add_user_and_token_opts(options, opts) + opts.on('--user-email EMAIL', 'Email used for token based authentication with Vizzy. Required') do |user_email| + options.user_email = user_email + end + + opts.on('--user-token TOKEN', 'Token used for token based authentication with Vizzy. NOTE: Only admins can create tokens. Required') do |user_token| + options.user_token = user_token + end +end + +def create_command + options = OpenStruct.new + options.title = nil + options.project = nil + options.commit = nil + options.file = nil + options.pull_request_number = -1 + options.url = nil + options.dev_build = false + options.user_email = nil + options.user_token = nil + + optparse = OptionParser.new do |opts| + opts.banner = 'Usage: upload_images_to_server.rb create [options]' + opts.on('-t', '--title TITLE', 'Title to supply for this build. Common usage is to match your CIs plan name + number') do |title| + options.title = title + end + + opts.on('-p', '--project ID', 'The project id to upload to. Required') do |id| + options.project = id + end + + opts.on('-c', '--commit HASH', 'Current git hash corresponding to this build. Required') do |hash| + options.commit = hash + end + + opts.on('-f', '--file BUILDFILE', 'File to store the created build information in. Used when running the "upload" command to commit the build. Required') do |file| + options.file = file + end + + opts.on('--pull-request PR_NUMBER', 'Pull request number to associate with this build. If not supplied build is treated as a "develop" build.') do |prNum| + options.pull_request_number = prNum + end + + opts.on('-u', '--url URL', 'url to the CI plan that created this build') do |url| + options.url = url + end + + opts.on('-d', '--developer-build', 'Used when a developer wants to simulate a pull request and check images against the server. Creates a \'dummy\' build where preapprovals will not work. Build deletes itself after 48 hours') do + options.dev_build = true + end + + add_user_and_token_opts(options, opts) + + opts.on_tail('-h', '--help', 'Show this message') do + puts opts + exit(0) + end + end + + validate_arguments(optparse, options) do |parsed_options| + required_options = [:user_email, :user_token, :project, :file] + unless parsed_options[:dev_build] + required_options = required_options + [:title, :commit] + end + required_options + end + + # Remove existing build file + FileUtils.rm_f(options.file) + + setup_shared_server_client + + build_id_response = get_build_id(options) + if build_id_response.code != '201' + error_message = 'Build ID Request Failed' + begin + parsed_data = JSON.parse(build_id_response) + error_message = parsed_data['error'] unless parsed_data['error'].blank? + rescue + # ignored + end + fail_build(error_message) + else + json = build_id_response.body + puts "Build Info: #{json}" + File.write(options.file, json) + + id = JSON.parse(json)['id'] + puts "Link to in progress build: #{@server_url}/builds/#{id}" + end +end + +def print_build_summary(id, result) + new_test_count = result['new_tests_count'] + unapproved_diffs_count = result['unapproved_diffs_count'] + missing_tests_count = result['missing_tests_count'] + successful_tests_count = result['successful_test_count'] + + puts "-------------" + puts "Build Summary" + puts "-------------" + puts "New Tests: #{new_test_count}" + puts "Missing Tests: #{missing_tests_count}" + puts "Successful Tests: #{successful_tests_count}" + puts "Unapproved Diffs: #{unapproved_diffs_count}" + print_vizzy_link(id) + puts "-------------" +end + +def upload_command + options = OpenStruct.new + options.file = nil + options.directory = nil + options.exit_0_on_diffs = false + options.check_image_count = false + options.user_email = nil + options.user_token = nil + + optparse = OptionParser.new do |opts| + opts.banner = 'Usage: upload_images_to_server.rb upload [options]' + opts.on('-f', '--file BUILDFILE', 'File to store the created build information in. Used when running the "upload" command to commit the build.') do |file| + options.file = file + end + opts.on('-d', '--directory IMAGEDIRECTORY', 'Directory to find images to upload.') do |directory| + options.directory = directory + end + opts.on('-e', '--exit-0-on-diffs', 'Script will show as succeeded when diffs are present.') do + options.exit_0_on_diffs = true + end + opts.on('-c', '--check-image-count', 'Script will fail if the number of images generated is less than the number of base images - 200.') do + options.check_image_count = true + end + + add_user_and_token_opts(options, opts) + + opts.on_tail('-h', '--help', 'Show this message') do + puts opts + exit(0) + end + end + + validate_arguments(optparse, options) do + [:file, :directory, :user_email, :user_token] + end + setup_shared_server_client + + build_dict = JSON.parse(File.read(options.file)) + + id = build_dict['id'] + base_image_count = build_dict['base_image_count'] + puts "Uploading images to Vizzy build with ID: #{id}" + puts "Base Image Count: #{base_image_count}" + + fail_build('Image Folder Empty', id) unless File.directory?(options.directory) + + number_of_images_generated = get_test_images_count(options.directory) + puts "Number of images generated: #{number_of_images_generated}" + image_md5s = compute_image_md5s_json(options.directory) + puts "Number of md5s calculated: #{image_md5s[:count]}" + + puts "Listing Images Generated: #{image_md5s[:json]}" + images_to_upload = send_image_md5s(id, image_md5s[:json])['image_md5s'] + total_upload_count = images_to_upload.size + puts '==============================' + puts "Number of Images to upload: #{total_upload_count}" + puts "Images to upload: #{images_to_upload}" + puts '==============================' + + upload_images_to_build_with_id(id, images_to_upload, options.directory) + result = commit_build(id, total_upload_count) + print_build_summary(id, result) + + if options.check_image_count && number_of_images_generated < (base_image_count * 0.8) + error_message = "Did not generate enough images, number of images generated: #{number_of_images_generated} < (base image count: #{base_image_count} * 0.8)" + fail_build(error_message, id) + end + + unapproved_diffs_count = result['unapproved_diffs_count'] + + if options.exit_0_on_diffs || unapproved_diffs_count == 0 + puts 'Exit(0)' + exit(0) + else + puts 'In order to check in your code, please fix the tests/approve them, and rerun' + puts 'Exit(1)' + exit(1) + end +end + +def open_command + options = OpenStruct.new + options.file = nil + optparse = OptionParser.new do |opts| + opts.banner = 'Usage: upload_images_to_server.rb open [options]' + opts.on('-f', '--file BUILDFILE', 'Build file that contains the build information of the build you want to open.') do |file| + options.file = file + end + opts.on_tail('-h', '--help', 'Show this message') do + puts opts + exit(0) + end + end + + validate_arguments(optparse, options) do + [:file] + end + + unless File.exists?(options.file) + puts 'Build file does not exist. You must run \'create\' before running \'open\' for the file to be created properly.' + exit(1) + end + + build_dict = JSON.parse(File.read(options.file)) + id = build_dict['id'] + + url = "#{@server_url}/builds/#{id}" + + `open #{url}` +end + +def fail_command + options = OpenStruct.new + options.file = nil + options.message = nil + options.user_email = nil + options.user_token = nil + + optparse = OptionParser.new do |opts| + opts.banner = 'Usage: upload_images_to_server.rb fail [options]' + opts.on('-f', '--file BUILDFILE', 'File to store the created build information in. Used when running the "upload" command to commit the build.') do |file| + options.file = file + end + opts.on('-m', '--message FAILUREMESSAGE', 'Message to fail the build with. Server will use this message for slack and github status updates.') do |message| + options.message = message + end + + add_user_and_token_opts(options, opts) + + opts.on_tail('-h', '--help', 'Show this message') do + puts opts + exit(0) + end + end + + validate_arguments(optparse, options) do + [:file, :message, :user_email, :user_token] + end + setup_shared_server_client + + build_dict = JSON.parse(File.read(options.file)) + + id = build_dict['id'] + + fail_build(options.message, id) +end + +STDOUT.sync = true +STDERR.sync = true + +top_level_parser = OptionParser.new do |opts| + opts.banner = 'Usage: upload_images_to_server.rb command [options]' + opts.separator < 0 && test_case <= 3 + puts "Unrecognized test case #{test_case}" + exit(1) +end + +visual_host = if host_with_port.nil? + puts 'host_with_port is nil, falling back to http://localhost:3000' + 'http://localhost:3000' + else + puts "Host with port: #{host_with_port}" + host_with_port + end + +auth_creds = "--user-email john.doe@gmail.com --user-token ht2Cey1i9xbxH5jm-gpx" +system("curl -O #{visual_host}/upload_images_to_server.rb") + +create_arguments = "-p 1 -c #{git_sha} -f ./tmp-buildfile -t ANDROIDGITHUBBUILD-ANDRGITHUBPULLREQUEST-26740" +unless is_develop == '1' + create_arguments += " --pull-request #{pull_request_number}" +end +puts "ruby ./upload_images_to_server.rb create #{visual_host} #{create_arguments} #{auth_creds}" +system("ruby ./upload_images_to_server.rb create #{visual_host} #{create_arguments} #{auth_creds}") + +upload_args = "-f ./tmp-buildfile -d image_set#{test_case}" +upload_args.concat(' -e') if @is_system_test +puts "ruby ./upload_images_to_server.rb upload #{visual_host} #{upload_args} #{auth_creds}" +system("ruby ./upload_images_to_server.rb upload #{visual_host} #{upload_args} #{auth_creds}") + +# Clean up downloaded files to avoid confusion +system('rm ./upload_images_to_server.rb') \ No newline at end of file diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb new file mode 100644 index 0000000..d19212a --- /dev/null +++ b/test/application_system_test_case.rb @@ -0,0 +1,5 @@ +require "test_helper" + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :chrome, screen_size: [1400, 1400] +end diff --git a/test/controllers/.keep b/test/controllers/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/controllers/builds_controller_test.rb b/test/controllers/builds_controller_test.rb new file mode 100644 index 0000000..2ae5db9 --- /dev/null +++ b/test/controllers/builds_controller_test.rb @@ -0,0 +1,66 @@ +require 'test_helper' + +class BuildsControllerTest < ActionController::TestCase + include Devise::Test::ControllerHelpers + + setup do + sign_in with_test_user + @build = FactoryBot.create(:build) + @build.project = FactoryBot.create(:project) + image_md5s_hash = {} + image_md5s_hash['AddAndMoveGridRows/AddAndMoveGridRows_01_InitialGridView'] = '12c755932dad470548c5c47708101e3d' + image_md5s_hash['AddAndMoveGridRows/AddAndMoveGridRows_02_ActionsModeOpen'] = '18b6543d01070e5502c1d90d9227d8ca' + @build.full_list_of_image_md5s = image_md5s_hash + @build.save + end + + test 'should get index' do + get :index + assert_response :success + assert_not_nil assigns(:builds) + end + + test 'should get new' do + get :new + assert_response :success + end + + test 'should create build' do + pr_response = { + "user": { + "ldap_dn": "CN=john.doe,OU=Users,OU=domain,DC=domaininternal,DC=com" + }, + "head": { + "ref": "MOBILEANDROID-4659_FakeBranchToTestPullRequest" + } + } + + stub_request(:post, "#{SystemTestConfig.bamboo_base_url}/rest/api/latest/result/ANDROIDGITHUBBUILD-ANDRGITHUBPULLREQUEST-26740/comment"). + to_return(status: 200, body: "", headers: {}) + + stub_request(:get, "#{SystemTestConfig.github_root_url}/api/v3/repos/mobile/android/pulls/1704"). + to_return(body: pr_response.to_json) + + stub_request(:post, "#{SystemTestConfig.github_root_url}/api/v3/repos/mobile/android/statuses/d3e552976815db44ac05983a817f7d1c333ef98e"). + to_return(status: 200, body: "", headers: {}) + + assert_difference('Build.count') do + post :create, format: :json, params: {build: {title: @build.title, url: @build.title, project_id: @build.project_id, temporary: @build.temporary, commit_sha: @build.commit_sha, + pull_request_number: @build.pull_request_number, image_md5s: @build.image_md5s}} + end + + assert_response :created + end + + test 'should update build' do + patch :update, params: { id: @build, build: { title: @build.title, url: @build.url }, authenticity_token: set_form_authenticity_token } + assert_redirected_to build_path(assigns(:build)) + end + + test 'should destroy build' do + assert_difference('Build.count', -1) do + delete :destroy, params: { id: @build, authenticity_token: set_form_authenticity_token } + end + assert_redirected_to builds_path + end +end diff --git a/test/controllers/diffs_controller_test.rb b/test/controllers/diffs_controller_test.rb new file mode 100644 index 0000000..5a0199a --- /dev/null +++ b/test/controllers/diffs_controller_test.rb @@ -0,0 +1,36 @@ +require 'test_helper' + +class DiffsControllerTest < ActionController::TestCase + include Devise::Test::ControllerHelpers + + setup do + sign_in with_test_user + @diff = FactoryBot.create(:diff) + end + + test 'should get new' do + get :new + assert_response :success + end + + test 'should create build' do + assert_difference('Diff.count') do + post :create, params: { diff: { old_image_id: @diff.old_image_id, new_image_id: @diff.new_image_id, build_id: @diff.build_id }, authenticity_token: set_form_authenticity_token } + end + + assert_redirected_to diff_path(assigns(:diff)) + end + + test 'should update diff' do + patch :update, params: {id: @diff, diff: { old_image_id: @diff.old_image_id, new_image_id: @diff.new_image_id }, authenticity_token: set_form_authenticity_token } + assert_redirected_to diff_path(assigns(:diff)) + end + + + test 'should destroy diff' do + assert_difference('Diff.count', -1) do + delete :destroy, params: { id: @diff, authenticity_token: set_form_authenticity_token } + end + assert_redirected_to diffs_path + end +end diff --git a/test/controllers/jiras_controller_test.rb b/test/controllers/jiras_controller_test.rb new file mode 100644 index 0000000..67aa95b --- /dev/null +++ b/test/controllers/jiras_controller_test.rb @@ -0,0 +1,53 @@ +require 'test_helper' + +class JirasControllerTest < ActionController::TestCase + include Devise::Test::ControllerHelpers + + setup do + sign_in with_test_user + @jira = FactoryBot.create(:jira) + end + + test 'should get index' do + get :index + assert_response :success + assert_not_nil assigns(:jiras) + end + + test 'should get new' do + get :new, params: { jira: { title: @jira.title } } + assert_response :success + end + + test 'should create jira' do + hash = { success: SystemTestConfig.jira_base_url} + @controller.stub(:create_jira, hash) do + assert_difference('Jira.count') do + post :create, params: {jira: {title: @jira.title, jira_link: @jira.jira_link}, authenticity_token: set_form_authenticity_token } + end + end + + assert_redirected_to @jira.jira_link + end + + test 'should show jira' do + get :show, params: { id: @jira } + assert_response :success + end + + test 'should update jira' do + hash = { success: SystemTestConfig.jira_base_url} + @controller.stub(:create_jira, hash) do + patch :update, params: {id: @jira, jira: {title: @jira.title}, authenticity_token: set_form_authenticity_token } + end + assert_redirected_to @jira.jira_link + end + + test 'should destroy jira' do + assert_difference('Jira.count', -1) do + delete :destroy, params: { id: @jira, authenticity_token: set_form_authenticity_token } + end + + assert_redirected_to jiras_path + end +end diff --git a/test/controllers/projects_controller_test.rb b/test/controllers/projects_controller_test.rb new file mode 100644 index 0000000..0a27ebc --- /dev/null +++ b/test/controllers/projects_controller_test.rb @@ -0,0 +1,43 @@ +require 'test_helper' + +class ProjectsControllerTest < ActionController::TestCase + include Devise::Test::ControllerHelpers + + setup do + # Must be admin to destroy project + sign_in with_test_user + @project = FactoryBot.create(:project) + end + + test 'should get index' do + get :index + assert_response :success + assert_not_nil assigns(:projects) + end + + test 'should get new' do + get :new + assert_response :success + end + + test 'should create project' do + assert_difference('Project.count') do + post :create, params: { project: { name: @project.name, description: @project.description }, authenticity_token: set_form_authenticity_token } + end + + assert_redirected_to project_path(assigns(:project)) + end + + test 'should update project' do + patch :update, params: { id: @project, project: { name: @project.name }, authenticity_token: set_form_authenticity_token } + assert_redirected_to project_path(assigns(:project)) + end + + test 'should destroy project' do + assert_difference('Project.count', -1) do + delete :destroy, params: { id: @project, authenticity_token: set_form_authenticity_token } + end + + assert_redirected_to projects_path + end +end diff --git a/test/controllers/test_images_controller_test.rb b/test/controllers/test_images_controller_test.rb new file mode 100644 index 0000000..a12d0e9 --- /dev/null +++ b/test/controllers/test_images_controller_test.rb @@ -0,0 +1,26 @@ +require 'test_helper' + +class TestImagesControllerTest < ActionController::TestCase + include Devise::Test::ControllerHelpers + + setup do + sign_in with_test_user + @test_image = FactoryBot.create(:test_image) + @test_image.build = FactoryBot.create(:build) + @test_image.save + end + + test 'should get index' do + get :index + assert_response :success + assert_not_nil assigns(:test_images) + end + + test 'should destroy test_image' do + assert_difference('TestImage.count', -1) do + delete :destroy, params: { id: @test_image, authenticity_token: set_form_authenticity_token } + end + + assert_redirected_to test_images_path + end +end diff --git a/test/controllers/tests_controller_test.rb b/test/controllers/tests_controller_test.rb new file mode 100644 index 0000000..61d48da --- /dev/null +++ b/test/controllers/tests_controller_test.rb @@ -0,0 +1,35 @@ +require 'test_helper' + +class TestsControllerTest < ActionController::TestCase + include Devise::Test::ControllerHelpers + + setup do + sign_in with_test_user + @test = FactoryBot.create(:test) + end + + test "should get index" do + get :index + assert_response :success + assert_not_nil assigns(:tests) + end + + test "should get new" do + get :new + assert_response :success + end + + test "should create test" do + assert_difference('Test.count') do + post :create, params: { test: { name: @test.name, description: @test.description }, authenticity_token: set_form_authenticity_token } + end + assert_redirected_to test_path(assigns(:test)) + end + + test "should destroy test" do + assert_difference('Test.count', -1) do + delete :destroy, params: { id: @test, authenticity_token: set_form_authenticity_token } + end + assert_redirected_to tests_path + end +end diff --git a/test/controllers/users_controller_test.rb b/test/controllers/users_controller_test.rb new file mode 100644 index 0000000..9f603fc --- /dev/null +++ b/test/controllers/users_controller_test.rb @@ -0,0 +1,20 @@ +require 'test_helper' + +class UsersControllerTest < ActionDispatch::IntegrationTest + include Devise::Test::IntegrationHelpers + + setup do + @user = with_test_user + sign_in @user + end + + test 'should get index user' do + get users_path + assert_response :success + end + + test 'should get show user' do + get user_path(id: @user.id) + assert_response :success + end +end diff --git a/test/factories/builds.rb b/test/factories/builds.rb new file mode 100644 index 0000000..784a82b --- /dev/null +++ b/test/factories/builds.rb @@ -0,0 +1,12 @@ +FactoryBot.define do + factory :build do + url "#{SystemTestConfig.bamboo_base_url}/browse/ANDROIDGITHUBBUILD-ANDRGITHUBPULLREQUEST-26740" + temporary false + title 'ANDROIDGITHUBBUILD-ANDRGITHUBPULLREQUEST-26740' + project_id 1 + commit_sha 'd3e552976815db44ac05983a817f7d1c333ef98e' + pull_request_number 1704 + image_md5s '000000.png=>5d06899520fdbe8fccf6e83eb33998d0' + username 'john.doe' + end +end \ No newline at end of file diff --git a/test/factories/diffs.rb b/test/factories/diffs.rb new file mode 100644 index 0000000..1ad69de --- /dev/null +++ b/test/factories/diffs.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :diff do + old_image_id 1 + new_image_id 2 + build_id 1 + end +end diff --git a/test/factories/jiras.rb b/test/factories/jiras.rb new file mode 100644 index 0000000..be25e6f --- /dev/null +++ b/test/factories/jiras.rb @@ -0,0 +1,13 @@ +FactoryBot.define do + factory :jira do + title 'Visual Automation Test Issue: LightColors_000000' + project SystemTestConfig.jira_project + component 'Visual Automation' + description 'https://vizzy.com/diffs/21839' + issue_type 'Bug' + jira_link "#{SystemTestConfig.jira_base_url}/browse/MOBILEANDROID-13135" + jira_key 'MOBILEANDROID-13135' + diff_id 1 + priority 'Critical' + end +end diff --git a/test/factories/projects.rb b/test/factories/projects.rb new file mode 100644 index 0000000..e86b2ca --- /dev/null +++ b/test/factories/projects.rb @@ -0,0 +1,69 @@ +def unique_id_for_plugin(plugin_name) + Rails.root.join('app', 'plugins', plugin_name).to_s.to_sym +end + +def bamboo_plugin_settings + { + enabled: true, + base_bamboo_build_url: { + value: SystemTestConfig.bamboo_base_url, + display_name: 'Base Bamboo Url', + placeholder: "Add base bamboo build url (e.g., 'https://bamboo.com')" + } + } +end + +def slack_plugin_settings + { + enabled: true, + slack_channel: { + value: 'test_channel', + display_name: 'Slack Channel', + placeholder: "Add slack channel to send build results to (e.g., '#build-status')" + } + } +end + +def jira_plugin_settings + { + enabled: true, + jira_base_url: { + value: SystemTestConfig.jira_base_url, + display_name: 'Jira Base Url', + placeholder: "Add jira root url (e.g., 'https://jira.com')" + }, + jira_project: { + value: SystemTestConfig.jira_project, + display_name: 'Jira Project', + placeholder: "Add jira project to file tickets (e.g., 'MOBILE')" + }, + jira_component: { + value: 'Visual Automation', + display_name: 'Jira Component', + placeholder: "Add jira component to file tickets (e.g., 'Visual Automation')" + } + } +end + +def jenkins_plugin_settings + { + enabled: false + } +end + +FactoryBot.define do + factory :project do + name 'Testing' + description 'A Testing Plan' + github_root_url SystemTestConfig.github_root_url + github_repo SystemTestConfig.github_repo + github_status_context 'continuous-integration/vizzy' + plugin_settings = { + unique_id_for_plugin('bamboo_plugin.rb') => bamboo_plugin_settings, + unique_id_for_plugin('slack_plugin.rb') => slack_plugin_settings, + unique_id_for_plugin('jira_plugin.rb') => jira_plugin_settings, + unique_id_for_plugin('jenkins_plugin.rb') => jenkins_plugin_settings + } + plugin_settings plugin_settings.deep_symbolize_keys! + end +end diff --git a/test/factories/test_images.rb b/test/factories/test_images.rb new file mode 100644 index 0000000..1c70a34 --- /dev/null +++ b/test/factories/test_images.rb @@ -0,0 +1,10 @@ +FactoryBot.define do + factory :test_image do + image_file_name '000000.png' + approved false + build_id 1 + test_id 2 + md5 '5d06899520fdbe8fccf6e83eb33998d0' + image { File.new(Rails.root.join('test-image-upload/image_set1/dark_colors/000000.png')) } + end +end diff --git a/test/factories/tests.rb b/test/factories/tests.rb new file mode 100644 index 0000000..9be0d20 --- /dev/null +++ b/test/factories/tests.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :test do + name 'Initial' + description 'First photo of the landing page' + association :project_id, factory: :project + end +end diff --git a/test/factories/users.rb b/test/factories/users.rb new file mode 100644 index 0000000..2d17df9 --- /dev/null +++ b/test/factories/users.rb @@ -0,0 +1,10 @@ +FactoryBot.define do + factory :user do + email 'john.doe@gmail.com' + username 'john.doe' + password '123456789012' + authentication_token 'ht2Cey1i9xbxH5jm-gpx' + admin true + initialize_with { User.find_or_create_by(email: email)} + end +end \ No newline at end of file diff --git a/test/helpers/.keep b/test/helpers/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/helpers/builds_helper_test.rb b/test/helpers/builds_helper_test.rb new file mode 100644 index 0000000..597641f --- /dev/null +++ b/test/helpers/builds_helper_test.rb @@ -0,0 +1,4 @@ +require 'test_helper' + +class BuildsHelperTest < ActionView::TestCase +end diff --git a/test/helpers/diffs_helper_test.rb b/test/helpers/diffs_helper_test.rb new file mode 100644 index 0000000..54dec01 --- /dev/null +++ b/test/helpers/diffs_helper_test.rb @@ -0,0 +1,4 @@ +require 'test_helper' + +class DiffsHelperTest < ActionView::TestCase +end diff --git a/test/helpers/images_helper_test.rb b/test/helpers/images_helper_test.rb new file mode 100644 index 0000000..f2a0cf2 --- /dev/null +++ b/test/helpers/images_helper_test.rb @@ -0,0 +1,4 @@ +require 'test_helper' + +class ImagesHelperTest < ActionView::TestCase +end diff --git a/test/helpers/jiras_helper_test.rb b/test/helpers/jiras_helper_test.rb new file mode 100644 index 0000000..04ceb43 --- /dev/null +++ b/test/helpers/jiras_helper_test.rb @@ -0,0 +1,4 @@ +require 'test_helper' + +class JirasHelperTest < ActionView::TestCase +end diff --git a/test/helpers/projects_helper_test.rb b/test/helpers/projects_helper_test.rb new file mode 100644 index 0000000..a591e4e --- /dev/null +++ b/test/helpers/projects_helper_test.rb @@ -0,0 +1,4 @@ +require 'test_helper' + +class ProjectsHelperTest < ActionView::TestCase +end diff --git a/test/helpers/tests_helper_test.rb b/test/helpers/tests_helper_test.rb new file mode 100644 index 0000000..59b6de2 --- /dev/null +++ b/test/helpers/tests_helper_test.rb @@ -0,0 +1,4 @@ +require 'test_helper' + +class TestsHelperTest < ActionView::TestCase +end diff --git a/test/integration/.keep b/test/integration/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/jobs/build_background_commit_job_test.rb b/test/jobs/build_background_commit_job_test.rb new file mode 100644 index 0000000..9d48ddd --- /dev/null +++ b/test/jobs/build_background_commit_job_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class BuildBackgroundCommitJobTest < ActiveJob::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/mailers/.keep b/test/mailers/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/models/.keep b/test/models/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/models/build_test.rb b/test/models/build_test.rb new file mode 100644 index 0000000..d995d76 --- /dev/null +++ b/test/models/build_test.rb @@ -0,0 +1,21 @@ +require 'test_helper' + +class BuildTest < ActiveSupport::TestCase + + setup do + @build = FactoryBot.create(:build) + end + + test 'build is valid after creation' do + assert @build.valid? + end + + test 'build is populated correctly' do + assert_equal "#{SystemTestConfig.bamboo_base_url}/browse/ANDROIDGITHUBBUILD-ANDRGITHUBPULLREQUEST-26740", @build.url, 'Url not populated correctly' + assert_equal false, @build.temporary, 'temporary not populated correctly' + assert_equal 'ANDROIDGITHUBBUILD-ANDRGITHUBPULLREQUEST-26740', @build.title, 'title not populated correctly' + assert_equal 'd3e552976815db44ac05983a817f7d1c333ef98e', @build.commit_sha, 'commit_sha not populated correctly' + assert_equal '1704', @build.pull_request_number, 'pull_request_number not populated correctly' + end + +end diff --git a/test/models/diff_test.rb b/test/models/diff_test.rb new file mode 100644 index 0000000..323ab05 --- /dev/null +++ b/test/models/diff_test.rb @@ -0,0 +1,17 @@ +require 'test_helper' + +class DiffTest < ActiveSupport::TestCase + setup do + @diff = FactoryBot.create(:diff) + end + + test 'diff is valid after creation' do + assert @diff.valid? + end + + test 'diff is populated correctly' do + assert_equal 1, @diff.old_image_id, 'old_image_id not populated correctly' + assert_equal 2, @diff.new_image_id, 'new_image_id not populated correctly' + assert_equal 1, @diff.build_id, 'build_id not populated correctly' + end +end diff --git a/test/models/image_test.rb b/test/models/image_test.rb new file mode 100644 index 0000000..819476f --- /dev/null +++ b/test/models/image_test.rb @@ -0,0 +1,20 @@ +require 'test_helper' +require 'digest/md5' + +class ImageTest < ActiveSupport::TestCase + + setup do + @test_image = FactoryBot.create(:test_image) + end + + test 'test image is valid after creation' do + assert @test_image.valid? + end + + test 'test image is populated correctly' do + assert_equal '000000.png', @test_image.image_file_name, 'image_file_name not populated correctly' + assert_equal false, @test_image.approved, 'approved not populated correctly' + assert_equal 1, @test_image.build_id, 'build_id not populated correctly' + assert_equal 2, @test_image.test_id, 'test_id not populated correctly' + end +end diff --git a/test/models/jira_test.rb b/test/models/jira_test.rb new file mode 100644 index 0000000..ab1d2c3 --- /dev/null +++ b/test/models/jira_test.rb @@ -0,0 +1,25 @@ +require 'test_helper' + +class JiraTest < ActiveSupport::TestCase + + setup do + @jira = FactoryBot.create(:jira) + end + + test 'Jira is valid after creation' do + assert @jira.valid? + end + + test 'jira is populated correctly' do + assert_equal 'Visual Automation Test Issue: LightColors_000000', @jira.title, 'title not populated correctly' + assert_equal SystemTestConfig.jira_project, @jira.project, 'project not populated correctly' + assert_equal 'Visual Automation', @jira.component, 'component not populated correctly' + assert_equal 'https://vizzy.com/diffs/21839', @jira.description, 'description not populated correctly' + assert_equal "#{SystemTestConfig.jira_base_url}/browse/MOBILEANDROID-13135", @jira.jira_link, 'jira_link not populated correctly' + assert_equal 'Bug', @jira.issue_type, 'issue_type not populated correctly' + assert_equal 'MOBILEANDROID-13135', @jira.jira_key, 'jira_key not populated correctly' + assert_equal 1, @jira.diff_id, 'diff_id not populated correctly' + assert_equal 'Critical', @jira.priority, 'priority not populated correctly' + end +end + diff --git a/test/models/project_test.rb b/test/models/project_test.rb new file mode 100644 index 0000000..00f7b59 --- /dev/null +++ b/test/models/project_test.rb @@ -0,0 +1,20 @@ +require 'test_helper' + +class ProjectTest < ActiveSupport::TestCase + + setup do + @project = FactoryBot.create(:project) + end + + test 'project is valid after creation' do + assert @project.valid? + end + + test 'project is populated correctly' do + assert_equal 'Testing', @project.name, 'name not populated correctly' + assert_equal 'A Testing Plan', @project.description, 'description not populated correctly' + assert_equal SystemTestConfig.github_root_url, @project.github_root_url, 'github_root_url not populated correctly' + assert_equal SystemTestConfig.github_repo, @project.github_repo, 'github_repo not populated correctly' + assert_equal 'continuous-integration/vizzy', @project.github_status_context, 'github_status_context not populated correctly' + end +end diff --git a/test/models/test_test.rb b/test/models/test_test.rb new file mode 100644 index 0000000..a756aaa --- /dev/null +++ b/test/models/test_test.rb @@ -0,0 +1,17 @@ +require 'test_helper' + +class TestTest < ActiveSupport::TestCase + + setup do + @test = FactoryBot.create(:test) + end + + test 'test is valid after creation' do + assert @test.valid? + end + + test 'test is populated correctly' do + assert_equal 'Initial', @test.name, 'name not populated correctly' + assert_equal 'First photo of the landing page', @test.description, 'description not populated correctly' + end +end diff --git a/test/models/user_test.rb b/test/models/user_test.rb new file mode 100644 index 0000000..9d3738f --- /dev/null +++ b/test/models/user_test.rb @@ -0,0 +1,10 @@ +require 'test_helper' + +class UserTest < ActiveSupport::TestCase + + test 'user is populated correctly' do + user = FactoryBot.build(:user) + assert_equal 'john.doe@gmail.com', user.email, 'email not populated correctly' + assert_equal 'john.doe', user.username, 'username not populated correctly' + end +end diff --git a/test/plugins/bamboo_plugin_test.rb b/test/plugins/bamboo_plugin_test.rb new file mode 100644 index 0000000..586a7ad --- /dev/null +++ b/test/plugins/bamboo_plugin_test.rb @@ -0,0 +1,40 @@ +require 'test_helper' + +class BambooPluginTest < ActiveSupport::TestCase + + def setup + @unique_id = Rails.root.join('app', 'plugins', 'bamboo_plugin.rb').to_s.to_sym + + stub_request(:post, "#{SystemTestConfig.bamboo_base_url}/rest/api/latest/result/ANDROIDGITHUBBUILD-ANDRGITHUBPULLREQUEST-26740/comment"). + to_return(status: 204) + + @build = FactoryBot.create(:build) + @build.project = FactoryBot.create(:project) + @build.save + + @bamboo_plugin = BambooPlugin.new(@unique_id) + @bamboo_plugin.add_plugin_settings_to_project(@build.project) + end + + test 'verify hook calls comment on build' do + mock = MiniTest::Mock.new + mock.expect(:comment_on_build, {success: 'Ok'}, [@build]) + @bamboo_plugin.build_created_hook(@build) + assert_send([mock, :comment_on_build, @build]) + end + + test 'bamboo build url has value' do + project_settings_hash = @build.project.plugin_settings + assert_equal(SystemTestConfig.bamboo_base_url, project_settings_hash[@bamboo_plugin.unique_id][:base_bamboo_build_url][:value]) + end + + test 'bamboo plugin loaded correctly' do + plugin_manager = PluginManager.instance.for_project(@build.project) + assert_not_nil(plugin_manager.plugins_hash[@unique_id]) + end + + test 'bamboo plugin credentials are present' do + assert_not_empty(Rails.application.secrets.BAMBOO_USERNAME) + assert_not_empty(Rails.application.secrets.BAMBOO_PASSWORD) + end +end \ No newline at end of file diff --git a/test/plugins/jenkins_plugin_test.rb b/test/plugins/jenkins_plugin_test.rb new file mode 100644 index 0000000..2890ad0 --- /dev/null +++ b/test/plugins/jenkins_plugin_test.rb @@ -0,0 +1,26 @@ +require 'test_helper' + +class JenkinsPluginTest < ActiveSupport::TestCase + + def setup + @unique_id = Rails.root.join('app', 'plugins', 'jenkins_plugin.rb').to_s.to_sym + + @build = FactoryBot.create(:build) + @build.project = FactoryBot.create(:project) + @build.save + + @jenkins_plugin = JenkinsPlugin.new(@unique_id) + end + + test 'verify hook calls update description' do + mock = MiniTest::Mock.new + mock.expect(:update_display_name_and_description, {success: 'Ok'}, [@build]) + @jenkins_plugin.build_created_hook(@build) + assert_send([mock, :update_display_name_and_description, @build]) + end + + test 'jenkins plugin loaded correctly' do + plugin_manager = PluginManager.instance.for_project(@build.project) + assert_not_nil(plugin_manager.plugins_hash[@unique_id]) + end +end \ No newline at end of file diff --git a/test/plugins/jira_plugin_test.rb b/test/plugins/jira_plugin_test.rb new file mode 100644 index 0000000..393f607 --- /dev/null +++ b/test/plugins/jira_plugin_test.rb @@ -0,0 +1,41 @@ +require 'test_helper' + +class JiraPluginTest < ActiveSupport::TestCase + + def setup + @unique_id = Rails.root.join('app', 'plugins', 'jira_plugin.rb').to_s.to_sym + + @build = FactoryBot.create(:build) + @build.project = FactoryBot.create(:project) + @build.save + + @jira = FactoryBot.create(:jira) + + @jira_plugin = JiraPlugin.new(@unique_id) + @jira_plugin.add_plugin_settings_to_project(@build.project) + + @project_settings_hash = @build.project.plugin_settings + end + + test 'jira base url has value' do + assert_equal(SystemTestConfig.jira_base_url, @project_settings_hash[@jira_plugin.unique_id][:jira_base_url][:value]) + end + + test 'jira project has value' do + assert_equal(SystemTestConfig.jira_project, @project_settings_hash[@jira_plugin.unique_id][:jira_project][:value]) + end + + test 'jira component has value' do + assert_equal('Visual Automation', @project_settings_hash[@jira_plugin.unique_id][:jira_component][:value]) + end + + test 'jira plugin loaded correctly' do + plugin_manager = PluginManager.instance.for_project(@build.project) + assert_not_nil(plugin_manager.plugins_hash[@unique_id]) + end + + test 'jira plugin credentials are present' do + assert_not_empty(Rails.application.secrets.JIRA_USERNAME) + assert_not_empty(Rails.application.secrets.JIRA_PASSWORD) + end +end \ No newline at end of file diff --git a/test/plugins/slack_plugin_test.rb b/test/plugins/slack_plugin_test.rb new file mode 100644 index 0000000..330770b --- /dev/null +++ b/test/plugins/slack_plugin_test.rb @@ -0,0 +1,42 @@ +require 'test_helper' + +class SlackPluginTest < ActiveSupport::TestCase + + def setup + @unique_id = Rails.root.join('app', 'plugins', 'slack_plugin.rb').to_s.to_sym + + @build = FactoryBot.create(:build) + @build.project = FactoryBot.create(:project) + @build.save + + @slack_plugin = SlackPlugin.new(@unique_id) + @slack_plugin.add_plugin_settings_to_project(@build.project) + end + + test 'create slack plugin and launch post request' do + assert_not_empty(Rails.application.secrets.SLACK_WEBHOOK) + slack_hook_url = "https://hooks.slack.com#{Rails.application.secrets.SLACK_WEBHOOK}" + + stub_request(:post, slack_hook_url). + to_return(status: 200, body: "", headers: {}) + + mock = MiniTest::Mock.new + mock.expect(:send_build_commit_slack_update, {success: 'Message Sent'}, [@build]) + @slack_plugin.build_committed_hook(@build) + assert_send([mock, :send_build_commit_slack_update, @build]) + + build_failed_slack_response = @slack_plugin.build_failed_hook(@build) + assert_equal(true, build_failed_slack_response.key?(:success)) + end + + test 'slack channel plugin setting has correct value' do + project_settings_hash = @build.project.plugin_settings + value = project_settings_hash[@slack_plugin.unique_id][:slack_channel][:value] + assert_equal('test_channel', value) + end + + test 'slack plugin loaded correctly' do + plugin_manager = PluginManager.instance.for_project(@build.project) + assert_not_nil(plugin_manager.plugins_hash[@unique_id]) + end +end \ No newline at end of file diff --git a/test/system/builds_test.rb b/test/system/builds_test.rb new file mode 100644 index 0000000..6633a27 --- /dev/null +++ b/test/system/builds_test.rb @@ -0,0 +1,134 @@ +require 'application_system_test_case' +require 'test_helper' + +class BuildsTest < ApplicationSystemTestCase + test 'develop 1, pr 1 should cause no diffs' do + initialize_build_test + + run_test_build('develop_1') + + visit_last_created_build + assert_images_checked(7) + assert_differences_found(0) + assert_new_tests(7) + assert_no_visual_differences_found + + run_test_build('pull_request_1') + + visit_last_created_build + take_screenshot + assert_images_checked(7) + assert_differences_found(0) + assert_new_tests(0) + assert_successful_tests(7) + end + + test 'develop 1, pr 2, pr 2, pr 3 should cause diffs' do + initialize_build_test + + run_test_build('develop_1') + run_test_build('pull_request_2') + + visit_last_created_build + assert_images_checked(7) + assert_differences_found(7) + assert_new_tests(0) + + open_first_unapproved_diff + assert_diffs_page_has_all_content + + approve_current_diff + click_button('Next') + approve_current_diff + approve_current_diff + + # verify that fields on diff page work correctly + fill_in('test_comment', with: 'Here is a test comment.') + fill_in('test_jira', with: "#{SystemTestConfig.jira_base_url}/browse/MOBILEANDROID-1234") + fill_in('test_pull_request_link', with: "#{SystemTestConfig.github_root_url}/mobile/android/pulls/2") + click_button('Save') + visit_last_created_build + page.must_have_content('Here is a test comment.') + page.must_have_content("#{SystemTestConfig.jira_base_url}/browse/MOBILEANDROID-1234") + page.must_have_content("#{SystemTestConfig.github_root_url}/mobile/android/pulls/2") + + # Assert that running the same pr build again gives same state as you left it + run_test_build('pull_request_2') + visit_last_created_build + assert_images_checked(7) + assert_differences_found(7) + assert_new_tests(0) + assert_diffs_approved(3) + assert_diffs_waiting_for_approval(4) + + # Assert PR 3 is unrelated to PR 2 + # Note that it only uploads 6 images instead of 7 + run_test_build('pull_request_3') + visit_last_created_build + assert_images_checked(6) + assert_differences_found(6) + assert_new_tests(0) + assert_missing_tests(1) + end + + test 'develop 1, pr 2, develop 2 performs preapproval' do + initialize_build_test + + run_test_build('develop_1') + run_preapproval_pull_request('pull_request_2') + + visit_last_created_build + open_first_unapproved_diff + approve_current_diff + approve_current_diff + approve_current_diff + + run_preapproval_develop('develop_2') + visit_last_created_build + assert_images_checked(7) + assert_differences_found(4) + assert_new_tests(0) + assert_successful_tests(3) + assert_diffs_waiting_for_approval(4) + end + + test 'develop 1, pr 2, pr3, develop 2 performs multiple preapproval' do + initialize_build_test + run_test_build('develop_1') + + run_preapproval_pull_request('pull_request_2') + visit_last_created_build + approve_all_diffs + + run_preapproval_pull_request('pull_request_3') + visit_last_created_build + approve_all_diffs + + run_preapproval_develop('develop_2') + visit_last_created_build + assert_images_checked(7) + assert_differences_found(12) + assert_new_tests(0) + assert_successful_tests(1) + assert_diffs_waiting_for_approval(12) + open_first_unapproved_diff + + # multiple preapproval diff page + page.must_have_button('Approve This Old Image') + page.must_have_content('Old Image Pre-Approved By:') + page.must_have_content('User: john.doe') + page.must_have_content("Pull Request: #{SystemTestConfig.github_root_url}/mobile/android/pull/2") + page.must_have_content("Pull Request: #{SystemTestConfig.github_root_url}/mobile/android/pull/3") + + approve_current_diff + approve_current_diff + + run_preapproval_develop('develop_2') + visit_last_created_build + assert_images_checked(7) + assert_differences_found(8) + assert_new_tests(0) + assert_successful_tests(3) + assert_diffs_waiting_for_approval(8) + end +end diff --git a/test/system/jiras_test.rb b/test/system/jiras_test.rb new file mode 100644 index 0000000..871a51c --- /dev/null +++ b/test/system/jiras_test.rb @@ -0,0 +1,27 @@ +require 'application_system_test_case' +require 'test_helper' + +class JirasTest < ApplicationSystemTestCase + test "create jira from diff" do + initialize_build_test + + run_test_build('develop_1') + run_test_build('pull_request_2') + visit_last_created_build + open_first_unapproved_diff + click_on('Create Jira') + + page.must_have_content('New Jira') + page.must_have_content('Project') + page.must_have_content('Jira Title') + page.must_have_content('Component') + page.must_have_content('Issue Type') + page.must_have_content('Priority') + page.must_have_content('Description') + page.must_have_content('Images attached') + page.must_have_button('Create Jira') + + click_on('Create Jira') + page.must_have_content('Creating issues is turned off') + end +end diff --git a/test/system/projects_test.rb b/test/system/projects_test.rb new file mode 100644 index 0000000..34f6619 --- /dev/null +++ b/test/system/projects_test.rb @@ -0,0 +1,53 @@ +require 'application_system_test_case' +require 'test_helper' + +class ProjectsTest < ApplicationSystemTestCase + test 'login, create project, view user' do + WebMock.allow_net_connect! + clear_database + + visit root_path + assert page.must_have_content('Log In') + login + + assert page.must_have_content('Signed in successfully.') + assert page.must_have_content('Projects') + create_project + assert page.must_have_content('Project was successfully created.') + + run_test_build('develop_1') + run_test_build('pull_request_2') + visit project_path(1) + + assert page.must_have_content('A Testing Plan') + assert page.must_have_content('Latest Pull Requests') + assert page.must_have_content('Latest Builds') + assert page.must_have_button('Current Base Images') + assert page.must_have_content('MOBILEANDROID-1840_ClearImageCacheOnLogout') + assert page.must_have_content('ANDROIDGITHUBBUILD-ANDRGITHUBPULLREQUEST-26740') + + click_on('Current Base Images') + assert page.must_have_content('Base Images') + assert page.must_have_content('light_colors') + assert page.must_have_content('000000') + assert page.must_have_content('000001') + assert page.must_have_content('000002') + click_on('000002') + + assert page.must_have_content('Test Details') + assert page.must_have_content('Name: 000002') + assert page.must_have_content('Current Base Image') + assert page.must_have_content('ANDROIDGITHUBBUILD-ANDRGITHUBPULLREQUEST-26740') + assert page.must_have_content('Parent Test: light_colors') + + visit users_path + assert page.must_have_content('Users') + assert page.must_have_content('Username') + assert page.must_have_content('Email') + assert page.must_have_content('Role') + click_link('Admin') + assert page.must_have_content('User Info') + assert page.must_have_content('Email: john.doe@gmail.com') + assert page.must_have_content('Role: Admin') + end +end \ No newline at end of file diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..e1424a4 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,190 @@ +require File.expand_path('../../config/environment', __FILE__) +require 'rails/test_help' +require 'minitest/rails/capybara' +require 'rake' +require 'webmock/minitest' +require 'minitest/mock' + +ENV['RAILS_ENV'] ||= 'test' +ENV['RAILS_SYSTEM_TESTING_SCREENSHOT'] = 'simple' + +Rake::Task.clear # necessary to avoid tasks being loaded several times in dev mode +VisualAutomation::Application.load_tasks # load rake tasks + +class ActiveSupport::TestCase + include FactoryBot::Syntax::Methods + + # Add more helper methods to be used by all tests here... + + # -------- Generic helper methods -------- # + + def login + user = with_test_user + fill_in('Email', with: user.email) + fill_in('Password', with: user.password) + click_button('Log In') + end + + def with_test_user + # test user must be an admin for all tests to pass + user = User.find_by_email('john.doe@gmail.com') + if user.nil? + puts "User john.doe@gmail.com does not exist, creating new user" + end + user = user.nil? ? FactoryBot.create(:user) : user + puts "User auth token: #{user.authentication_token}, email: #{user.email}, admin: #{user.admin}" + user + end + + # -------- Project helper methods -------- # + + def create_project + click_on('New Project') + find("input[id$='project_name']").set 'Testing' + find("input[id$='project_description']").set 'A Testing Plan' + find("input[id$='project_github_root_url']").set SystemTestConfig.github_root_url + find("input[id$='project_github_repo']").set SystemTestConfig.github_repo + find("input[id$='project_github_status_context']").set 'continuous-integration/vizzy' + + # plugin settings + jira_input_id = get_input_id_for_plugin('jira_plugin.rb') + find("input[id='#{jira_input_id}_jira_project_value']").set SystemTestConfig.jira_project + find("input[id='#{jira_input_id}_jira_base_url_value']").set SystemTestConfig.jira_base_url + find("input[id='#{get_input_id_for_plugin('bamboo_plugin.rb')}_base_bamboo_build_url_value']").set SystemTestConfig.bamboo_base_url + find("input[id='#{get_input_id_for_plugin('slack_plugin.rb')}_slack_channel_value']").set 'test_channel' + + click_on('Submit') + end + + def get_input_id_for_plugin(plugin) + rails_root_join = Rails.root.join('app', 'plugins', plugin).to_s.gsub('/', '_') + "plugin_settings_#{rails_root_join}" + end + + # -------- Build helper methods -------- # + + def initialize_build_test + WebMock.allow_net_connect! + + clear_database + + visit projects_path + login + FactoryBot.create(:project) + end + + def clear_database + # clear the database with the current schema + system("rails db:environment:set RAILS_ENV=#{ENV['RAILS_ENV']}") + system('rake db:schema:load') + end + + def run_test_build(build_ref) + rake_task, uri_with_port = get_build_params_and_reenable_rake_task(build_ref) + Rake.application[rake_task].invoke(uri_with_port) + end + + def run_preapproval_pull_request(build_ref) + rake_task, uri_with_port = get_build_params_and_reenable_rake_task(build_ref) + pull_request_number = build_ref.rpartition('_').last.to_i + pull_request_preapproval_git_sha = github_recent_commits[pull_request_number] + Rake.application[rake_task].invoke(uri_with_port, pull_request_preapproval_git_sha, pull_request_number) + end + + def run_preapproval_develop(build_ref) + rake_task, uri_with_port = get_build_params_and_reenable_rake_task(build_ref) + develop_most_recent_sha = github_recent_commits.first + Rake.application[rake_task].invoke(uri_with_port, develop_most_recent_sha) + end + + def visit_last_created_build + id = get_build_id + puts "Visiting build with id: #{id}" + visit build_path(id) + end + + def assert_diffs_page_has_all_content + page.must_have_button('Approve New Image') + page.must_have_button('Create Jira') + page.must_have_button('Next') + page.must_have_button('Base Images for Test') + page.must_have_button('Test History') + page.must_have_button('Save') + page.must_have_content('Jira') + page.must_have_content('Pull Request') + page.must_have_content('Comment') + end + + def assert_successful_tests(number) + page.must_have_content("#{number} successful test(s)") + end + + def assert_images_checked(number) + page.must_have_content("#{number} images checked") + end + + def assert_differences_found(number) + page.must_have_content("#{number} difference(s) found") + end + + def assert_new_tests(number) + page.must_have_content("#{number} new test(s) added") + end + + def assert_missing_tests(number) + page.must_have_content("#{number} missing test(s)") + end + + def assert_no_visual_differences_found + page.must_have_content 'No visual differences were found between this build and the base images.' + end + + def assert_diffs_waiting_for_approval(number) + page.must_have_content("#{number} diffs waiting for approval") + end + + def assert_diffs_approved(number) + page.must_have_content("#{number} diffs approved") + end + + def open_first_unapproved_diff + build = Build.find(get_build_id) + first_diff = build.unapproved_diffs.first + visit diff_path(first_diff.id) + end + + def approve_current_diff + click_button('Approve New Image') + end + + def approve_all_diffs + click_button('Approve All Images') + end + + private + def get_build_id + temp_build_file = File.read(Rails.root.join('test-image-upload', 'tmp-buildfile')) + data = JSON.parse(temp_build_file) + data['id'] + end + + def get_build_params_and_reenable_rake_task(build_ref) + current_url = URI.parse(page.current_url) + uri_with_port = "http://#{current_url.host}:#{current_url.port}" + rake_task = "run_test_build:#{build_ref}" + Rake.application[rake_task].reenable + [rake_task, uri_with_port] + end + + def github_recent_commits + associated_git_shas = [] + GithubService.run(Project.find(1).github_root_url, Project.find(1).github_repo) do |service| + associated_git_shas = service.most_recent_github_commits + end + associated_git_shas + end + + def set_form_authenticity_token + session[:_csrf_token] = SecureRandom.base64(32) + end +end