├── doc └── images │ ├── lambdacd-lineup-1.png │ └── lambdacd-lineup-2.png ├── .gitignore ├── resources ├── logback.xml ├── lineup_screenshot.rb └── lineup_compare.rb ├── src └── lambdacd_lineup │ ├── util.clj │ ├── io.clj │ ├── config.clj │ └── core.clj ├── project.clj ├── example ├── resources │ └── lineup.json └── example_pipeline │ └── pipeline.clj ├── test └── lambdacd_lineup │ ├── util_test.clj │ ├── core_test.clj │ └── config_test.clj ├── README.md └── LICENSE.md /doc/images/lambdacd-lineup-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/otto-de-legacy/lambdacd-lineup/master/doc/images/lambdacd-lineup-1.png -------------------------------------------------------------------------------- /doc/images/lambdacd-lineup-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/otto-de-legacy/lambdacd-lineup/master/doc/images/lambdacd-lineup-2.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | .idea 13 | lambdacd-lineup.iml 14 | -------------------------------------------------------------------------------- /resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/lambdacd_lineup/util.clj: -------------------------------------------------------------------------------- 1 | (ns lambdacd-lineup.util 2 | (:require [clojure.string :as s])) 3 | 4 | (defn replace-env-in-url [url env env-mapping] 5 | (let [env-mapped (get env-mapping env)] 6 | (if (nil? env-mapped) 7 | (s/replace url #"#env#" env) 8 | (s/replace url #"#env#" env-mapped)))) 9 | 10 | (defn replace-special-chars-in-url [url] 11 | (s/replace url #"[^a-zA-Z0-9]+" "_")) 12 | 13 | (defn concat-url-and-path [url path] 14 | (if (= path "/") 15 | (str url path) 16 | (str url "/" path))) 17 | -------------------------------------------------------------------------------- /resources/lineup_screenshot.rb: -------------------------------------------------------------------------------- 1 | require 'lineup' 2 | 3 | lineup = Lineup::Screenshot.new(ARGV[0]) 4 | lineup.resolutions(ARGV[1]) 5 | lineup.urls(ARGV[2]) 6 | lineup.filepath_for_images(ARGV[3]) 7 | lineup.use_phantomjs(ARGV[4] == "true") 8 | lineup.wait_for_asynchron_pages(ARGV[5].to_i) 9 | 10 | cookies = JSON.parse(ARGV[6]) 11 | cookies_with_symbol_keys = [] 12 | cookies.each do |cookie| 13 | cookies_with_symbol_keys.push(cookie.inject({}) { |element, (symbol, value)| element[symbol.to_sym] = value; element }) 14 | end 15 | 16 | lineup.cookies(cookies_with_symbol_keys) 17 | 18 | localStorage = JSON.parse(ARGV[7]) 19 | lineup.localStorage(localStorage) 20 | 21 | lineup.record_screenshot('before') -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject lambdacd-lineup "0.5.2" 2 | :description "LambdaCD library to integrate Lineup" 3 | :url "https://github.com/otto-de/lambdacd-lineup" 4 | :license {:name "Apache License 2.0" 5 | :url "http://www.apache.org/licenses/LICENSE-2.0"} 6 | :scm {:name "git" 7 | :url "https://github.com/otto-de/lambdacd-lineup"} 8 | :test-paths ["test", "example"] 9 | :dependencies [[org.clojure/clojure "1.7.0"] 10 | [lambdacd "0.6.1"] 11 | [lambdacd-artifacts "0.1.0"] 12 | [bouncer "0.3.3"] 13 | [org.clojure/core.incubator "0.1.3"] 14 | [clj-http "2.0.0"] 15 | [clj-http-fake "1.0.1"]] 16 | :profiles {:uberjar {:aot :all}} 17 | :deploy-repositories [["clojars" {:creds :gpg}]] 18 | :main example-pipeline.pipeline) 19 | -------------------------------------------------------------------------------- /resources/lineup_compare.rb: -------------------------------------------------------------------------------- 1 | require 'lineup' 2 | 3 | lineup = Lineup::Screenshot.new(ARGV[0]) 4 | lineup.resolutions(ARGV[1]) 5 | lineup.urls(ARGV[2]) 6 | lineup.filepath_for_images(ARGV[3]) 7 | lineup.difference_path(ARGV[3]) 8 | lineup.use_phantomjs(ARGV[4] == "true") 9 | lineup.wait_for_asynchron_pages(ARGV[5].to_i) 10 | 11 | cookies = JSON.parse(ARGV[6]) 12 | cookies_with_symbol_keys = [] 13 | cookies.each do |cookie| 14 | cookies_with_symbol_keys.push(cookie.inject({}) { |element, (symbol, value)| element[symbol.to_sym] = value; element }) 15 | end 16 | lineup.cookies(cookies_with_symbol_keys) 17 | 18 | localStorage = JSON.parse(ARGV[7]) 19 | lineup.localStorage(localStorage) 20 | 21 | lineup.record_screenshot('after') 22 | 23 | begin 24 | lineup.compare('before', 'after') 25 | lineup.save_json(ARGV[3]) 26 | rescue RuntimeError => e 27 | puts ARGV[0] 28 | puts e 29 | end 30 | 31 | 32 | -------------------------------------------------------------------------------- /example/resources/lineup.json: -------------------------------------------------------------------------------- 1 | { 2 | "urls": { 3 | "https://#env#.otto.de": { 4 | "paths": [ 5 | "/", 6 | "multimedia" 7 | ], 8 | "max-diff": 2, 9 | "cookies": [ 10 | { 11 | "name": "enableAwesomeFeature1", 12 | "value": "true" 13 | }, 14 | { 15 | "name": "enableAwesomeFeature2", 16 | "value": "false" 17 | } 18 | ], 19 | "local-storage" : { 20 | "someKey1" : "someValue1", 21 | "someKey2" : "someValue2" 22 | }, 23 | "env-mapping": { 24 | "live": "www" 25 | }, 26 | "resolutions": [ 27 | 970 28 | ] 29 | }, 30 | "http://#env#.ottogroup.com": { 31 | "paths": [ 32 | "de" 33 | ], 34 | "max-diff": 2, 35 | "env-mapping": { 36 | "live": "www" 37 | } 38 | } 39 | }, 40 | "browser": "firefox", 41 | "resolutions": [ 42 | 600, 43 | 800 44 | ] 45 | } -------------------------------------------------------------------------------- /src/lambdacd_lineup/io.clj: -------------------------------------------------------------------------------- 1 | (ns lambdacd-lineup.io 2 | (:require [clojure.java.io :as io] 3 | [cheshire.core :as cheshire] 4 | [lambdacd-lineup.util :as util] 5 | [lambdacd-artifacts.core :as artifacts]) 6 | (:import (java.io File))) 7 | 8 | (defn ensure-dir [parent dirname] 9 | (let [d (io/file parent dirname)] 10 | (.mkdirs d) 11 | d)) 12 | 13 | (defn copy-to [dir resource-name] 14 | (let [res-file-name (last (.split resource-name "/")) 15 | out-filename (str dir "/" res-file-name)] 16 | (with-open [in (io/input-stream (io/resource resource-name))] 17 | (io/copy in (io/file out-filename))))) 18 | 19 | (defn load-config-file [resource-name] 20 | (cheshire/parse-string (slurp (io/resource resource-name)))) 21 | 22 | (defn lineup-json-exists [url home-dir build-number step-id] 23 | (let [dir (str home-dir "/" build-number "/" (artifacts/format-step-id step-id)) 24 | url-for-dir (util/replace-special-chars-in-url url)] 25 | (.exists 26 | (io/as-file (str dir "/" url-for-dir "_log.json"))))) 27 | -------------------------------------------------------------------------------- /example/example_pipeline/pipeline.clj: -------------------------------------------------------------------------------- 1 | (ns example-pipeline.pipeline 2 | (:use [lambdacd.steps.manualtrigger]) 3 | (:require 4 | [ring.server.standalone :as ring-server] 5 | [lambdacd.ui.ui-server :as ui] 6 | [lambdacd.runners :as runners] 7 | [lambdacd.util :as util] 8 | [clojure.tools.logging :as log] 9 | [compojure.core :refer :all] 10 | [lambdacd-artifacts.core :as artifacts] 11 | [lambdacd-lineup.core :as lineup] 12 | [lambdacd-lineup.io :as io]) 13 | (:gen-class)) 14 | 15 | (def pipeline-def 16 | `( 17 | wait-for-manual-trigger 18 | (lineup/take-screenshots "live") 19 | (lambdacd.steps.control-flow/either 20 | (lambdacd.steps.control-flow/run (lineup/compare-with-screenshots "live") 21 | (lineup/analyse-comparison "live")) 22 | wait-for-manual-trigger) 23 | )) 24 | 25 | (defn -main [& args] 26 | (let [home-dir (util/create-temp-dir) 27 | artifacts-path-context "/artifacts" 28 | lineup-cfg (io/load-config-file "resources/lineup.json") 29 | config {:lineup-cfg lineup-cfg 30 | :home-dir home-dir 31 | :dont-wait-for-completion false 32 | :artifacts-path-context artifacts-path-context} 33 | pipeline (lambdacd.core/assemble-pipeline pipeline-def config) 34 | app (ui/ui-for pipeline)] 35 | (log/info "LambdaCD Home Directory is " home-dir) 36 | (runners/start-one-run-after-another pipeline) 37 | (ring-server/serve (routes 38 | (context "/pipeline" [] app) 39 | (context artifacts-path-context [] (artifacts/artifact-handler-for pipeline))) 40 | {:open-browser? false 41 | :port 8080}))) 42 | 43 | -------------------------------------------------------------------------------- /test/lambdacd_lineup/util_test.clj: -------------------------------------------------------------------------------- 1 | (ns lambdacd-lineup.util-test 2 | (:require [clojure.test :refer :all] 3 | [lambdacd-lineup.util :refer :all])) 4 | 5 | (deftest replace-env-in-url-test 6 | (testing "url wo env placeholder" 7 | (let [url "https://www.otto.de"] 8 | (is (= url (replace-env-in-url url "dev" nil))))) 9 | (testing "url with one env placeholder" 10 | (let [url "https://#env#.otto.de"] 11 | (is (= "https://dev.otto.de" (replace-env-in-url url "dev" nil))))) 12 | (testing "url with two env placeholder" 13 | (let [url "https://#env#.#env#.otto.de"] 14 | (is (= "https://dev.dev.otto.de" (replace-env-in-url url "dev" nil))))) 15 | (testing "url with wrong placeholder" 16 | (let [url "https://#myenv#.otto.de"] 17 | (is (= "https://#myenv#.otto.de" (replace-env-in-url url "dev" nil))))) 18 | (testing "url with wrong placeholder" 19 | (let [url "https://#env#.otto.de"] 20 | (is (= "https://www.otto.de" (replace-env-in-url url "live" {"live" "www"})))))) 21 | 22 | (deftest replace-special-chars-in-url-test 23 | (testing "url with dots" 24 | (let [url "www.otto.de"] 25 | (is (= "www_otto_de" (replace-special-chars-in-url url))))) 26 | (testing "typical url with special chars" 27 | (let [url "https://www.otto.de"] 28 | (is (= "https_www_otto_de" (replace-special-chars-in-url url))))) 29 | (testing "typical url with special chars" 30 | (let [url "https://www.otto-otto.de"] 31 | (is (= "https_www_otto_otto_de" (replace-special-chars-in-url url))))) 32 | (testing "typical url with special chars" 33 | (let [url "https://www.otto.de?myparam"] 34 | (is (= "https_www_otto_de_myparam" (replace-special-chars-in-url url))))) 35 | (testing "typical url with special chars" 36 | (let [url "https://www.otto.de#top"] 37 | (is (= "https_www_otto_de_top" (replace-special-chars-in-url url))))) 38 | (testing "typical url with special chars" 39 | (let [url "https://www.otto.de%test"] 40 | (is (= "https_www_otto_de_test" (replace-special-chars-in-url url)))))) 41 | 42 | (deftest concat-url-and-path-test 43 | (testing "url + damenmode" 44 | (is (= "https://www.otto.de/damenmode" 45 | (concat-url-and-path "https://www.otto.de" "damenmode")))) 46 | (testing "url storefront" 47 | (is (= "https://www.otto.de/" 48 | (concat-url-and-path "https://www.otto.de" "/"))))) 49 | -------------------------------------------------------------------------------- /test/lambdacd_lineup/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns lambdacd-lineup.core-test 2 | (:require [clojure.test :refer :all] 3 | [lambdacd-lineup.core :refer :all] 4 | [clj-http.fake :as clj-http-fake] 5 | [lambdacd.steps.shell :as shell])) 6 | 7 | (deftest check-status-code-for-one-url-test 8 | (testing "valid url damenmode" 9 | (is (empty? (check-status-code-for-one-url "https://www.otto.de/damenmode")))) 10 | (testing "invalid url" 11 | (is (= ["https://www.otto.de/test123"] 12 | (check-status-code-for-one-url "https://www.otto.de/test123")))) 13 | (testing "invalid host" 14 | (is (= ["https://www.ghgsdfhksfhks.de/damenmode"] 15 | (check-status-code-for-one-url "https://www.ghgsdfhksfhks.de/damenmode"))))) 16 | 17 | (deftest get-domain-from-url-test 18 | (testing "valid url: otto.de with http" 19 | (is (= ".otto.de" 20 | (get-domain-from-url "http://www.otto.de")))) 21 | (testing "valid url: otto.de with https" 22 | (is (= ".otto.de" 23 | (get-domain-from-url "https://www.otto.de")))) 24 | (testing "valid url: otto.de with http and port" 25 | (is (= ".otto.de" 26 | (get-domain-from-url "http://www.otto.de:80")))) 27 | (testing "valid url: otto.de with subdomain" 28 | (is (= ".otto.de" 29 | (get-domain-from-url "http://x.y.otto.de"))))) 30 | 31 | (deftest check-status-code-test 32 | (testing "3 valid paths" 33 | (is (empty? 34 | (clj-http-fake/with-fake-routes 35 | {"https://www.otto.de/" (fn [request] {:status 200 :headers {} :body "test"}) 36 | "https://www.otto.de/damenmode" (fn [request] {:status 200 :headers {} :body "test"}) 37 | "https://www.otto.de/multimedia" (fn [request] {:status 200 :headers {} :body "test"})} 38 | (check-status-code "https://www.otto.de" ["/" "damenmode" "multimedia"]))))) 39 | (testing "1 invalid path" 40 | (is (= ["https://www.otto.de/mytest"] 41 | (clj-http-fake/with-fake-routes 42 | {"https://www.otto.de/" (fn [request] {:status 200 :headers {} :body "test"}) 43 | "https://www.otto.de/mytest" (fn [request] {:status 404 :headers {} :body "test"}) 44 | "https://www.otto.de/multimedia" (fn [request] {:status 200 :headers {} :body "test"})} 45 | (check-status-code "https://www.otto.de" ["/" "mytest" "multimedia"]))))) 46 | (testing "invalid url" 47 | (is (= ["https://www.ghgsdfhksfhks.de/" "https://www.ghgsdfhksfhks.de/damenmode" "https://www.ghgsdfhksfhks.de/multimedia"] 48 | (check-status-code "https://www.ghgsdfhksfhks.de" ["/" "damenmode" "multimedia"]))))) 49 | 50 | (deftest calc-detected-max-diff-test 51 | (testing "empty vector" 52 | (let [json []] 53 | (is (= 0 (calc-detected-max-diff json))))) 54 | (testing "five entries" 55 | (let [json [{:difference 1.1142448209605373}, 56 | {:difference 48}, 57 | {:difference 1.5}, 58 | {:difference 9}, 59 | {:difference 1.8237236233}]] 60 | (is (= 48 (calc-detected-max-diff json)))))) 61 | 62 | (deftest calc-new-status-test 63 | (testing "success -> failure" 64 | (is (= {:status :failure} (calc-new-status {:status :success} 5 2)))) 65 | (testing "failure -> failure" 66 | (is (= {:status :failure} (calc-new-status {:status :failure} 1 2)))) 67 | (testing "success -> success" 68 | (is (= {:status :success} (calc-new-status {:status :success} 1 2))))) 69 | 70 | (deftest iterate-over-urls-test 71 | (let [last-resolution (atom nil) 72 | script-name "wurst" 73 | buildno 1 74 | env "live" 75 | ctx {} 76 | home-dir "home" 77 | printer nil 78 | ] 79 | (with-redefs [take-screenshots-output (fn [& _] nil) 80 | take-screenshot (fn [_ _ _ _ resolutions _ _ _ _ _ _] (reset! last-resolution resolutions))] 81 | (testing "should use fallback if no resolution is set" 82 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5}}}] 83 | (interate-urls-to-take-screenshots cfg script-name buildno env ctx home-dir printer) 84 | (is (= @last-resolution "1200"))) 85 | ) 86 | (testing "should use outer resolutions if no inner is given" 87 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5}} "resolutions" [800 900]}] 88 | (interate-urls-to-take-screenshots cfg script-name buildno env ctx home-dir printer) 89 | (is (= @last-resolution "800,900"))) 90 | ) 91 | 92 | (testing "should use inner resolutions if no outer is given" 93 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5 "resolutions" [800 900]}}}] 94 | (interate-urls-to-take-screenshots cfg script-name buildno env ctx home-dir printer) 95 | (is (= @last-resolution "800,900"))) 96 | ) 97 | 98 | (testing "should prefer inner resolutions over outer resolutions" 99 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5 "resolutions" [800 900]}} "resolutions" [10]}] 100 | (interate-urls-to-take-screenshots cfg script-name buildno env ctx home-dir printer) 101 | (is (= @last-resolution "800,900"))) 102 | )))) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LambdaCD-Lineup 2 | 3 | With LambdaCD-Lineup you can take and compare screenshots of multiple urls in your pipeline. This is very helpful if you have a webservice and you want to ensure that changes do not affect the GUI. 4 | 5 | [![Clojars Project](http://clojars.org/lambdacd-lineup/latest-version.svg)](http://clojars.org/lambdacd-lineup) 6 | 7 | Check out [Lineup](https://github.com/otto-de/lineup) to get more information about this project. 8 | LambdaCD-Lineup is just a wrapper to integrate this tool in LambdaCD. 9 | 10 | ## Requirements 11 | * Ruby 12 | * PhantomJS or headless Firefox 13 | * [Lineup](https://github.com/otto-de/lineup) >= 0.7.0 14 | 15 | Both must exist in any directory in your $PATH. 16 | ## Usage 17 | 18 | ```clojure 19 | (defn -main [& args] 20 | (let [home-dir (util/create-temp-dir) 21 | artifacts-path-context "/artifacts" 22 | lineup-cfg (lambdacd-lineup.io/load-config-file "lineup.json") 23 | config {:lineup-cfg lineup-cfg 24 | :home-dir home-dir 25 | :dont-wait-for-completion false 26 | :artifacts-path-context artifacts-path-context} 27 | pipeline (lambdacd.core/assemble-pipeline pipeline-def config) 28 | app (ui/ui-for pipeline)] 29 | (log/info "LambdaCD Home Directory is " home-dir) 30 | (runners/start-one-run-after-another pipeline) 31 | (ring-server/serve (routes 32 | (context "/pipeline" [] app) 33 | (context artifacts-path-context [] (artifacts/artifact-handler-for pipeline))) 34 | {:open-browser? false 35 | :port 8080}))) 36 | ``` 37 | 38 | ### Define the artifacts path 39 | ``` 40 | artifacts-path-context "/artifacts" 41 | ``` 42 | The LambdaCD-Artifacts plugin needs this path to serve your screenshots. 43 | 44 | ### Define your LambdaCD-Lineup config 45 | ```javascript 46 | { 47 | "urls": { 48 | "https://#env#.otto.de": { 49 | "paths": [ 50 | "/", 51 | "multimedia" 52 | ], 53 | "max-diff": 2, 54 | "env-mapping": { 55 | "live": "www" 56 | }, 57 | "cookies": [ 58 | { 59 | "name": "enableAwesomeFeature1", 60 | "value": "true" 61 | }, 62 | { 63 | "name": "enableAwesomeFeature2", 64 | "value": "false" 65 | } 66 | ], 67 | "local-storage" : { 68 | "key1": "value1", 69 | "key2": "value2" 70 | } 71 | "resolutions": [ 72 | 800, 73 | 1200 74 | ] 75 | }, 76 | "http://#env#.ottogroup.com" : { 77 | "paths": [ 78 | "de" 79 | ], 80 | "max-diff": 2 81 | } 82 | }, 83 | "browser": "firefox", 84 | "async-wait": 10, 85 | "resolutions": [ 86 | 600, 87 | 800 88 | ] 89 | } 90 | ``` 91 | * urls: Map of urls on configs (no default) 92 | * Key: URL without path (no defualt). A placehoder #env# can be used to inject a environment. 93 | * Value: 94 | * paths: Path to subsites. URL + "/" + paths = otto.de/sport, otto.de/media (default: "/") 95 | * max-diff: max difference between two screenshots (before and after) 96 | * env-mapping: This mapping will replace the environment (argument of take-screenshots and analyse-comparison) with the corresponding value in this map. 97 | Example: You call analyse-comparison live but in one url you need the string wwww 98 | * cookies: Set cookie for this url 99 | * name: Name of the cookie 100 | * value: Value of the cookie 101 | * path: Path of this cookie (optional, default: "/") 102 | * secure: Boolean. Only send cookie if you use https (optional, default: false) 103 | * resolutions: Width of the screenshots for this URL (overrules outer resolutions in config) 104 | * local-storage: Set local-storage key/value pairs in browser 105 | * resolutions: Width of the screenshots (default: 1200) 106 | * browser: "firefox" or "phantomjs" (default: :firefox) 107 | * async-wait: Time to wait in seconds between rendering the page and taking the screenshots. Useful to load resources (fonts,...) asynchronously (default: 5) 108 | 109 | ### LambdaCD config 110 | Add your lineup config and the artifacts-path-context to the LambdaCD config. 111 | 112 | ``` 113 | config {:lineup-cfg lineup-cfg 114 | :artifacts-path-context artifacts-path-context 115 | [...]} 116 | ``` 117 | ### Add LambdaCD-Lineup steps to your pipeline 118 | ```clojure 119 | (def pipeline-def 120 | `( 121 | [...] 122 | (lineup/take-screenshots "develop") 123 | deploy-my-app 124 | (lineup/compare-with-screenshots "develop") 125 | (lineup/analyse-comparison "develop") 126 | [...] 127 | )) 128 | ``` 129 | 130 | ### take-screenshots 131 | Execute this step before you deploy your changes. 132 | 133 | Parameters: 134 | * environment: develop, live, ... (default: www) 135 | 136 | Will be used in your base-url instead of placeholder #env# 137 | ### compare-with-screenshots 138 | Execute this step after you deploy your changes. 139 | 140 | This step compares the current version of your website with the screenshots taken in the step 'take-screenshots'. 141 | 142 | Parameters: 143 | * environment: develop, live, ... (default: www) 144 | 145 | Will be used in your base-url instead of placeholder #env# 146 | ### analyse-comparison 147 | Execute this step after you compare the versions. 148 | 149 | Parameter: 150 | * environment: develop, live, ... (default: www) 151 | 152 | Will be used in your base-url instead of placeholder #env# 153 | 154 | You can see the screenshots if you click on this step: 155 | 156 | ![ScreenShot](/doc/images/lambdacd-lineup-1.png) 157 | 158 | ![ScreenShot](/doc/images/lambdacd-lineup-2.png) 159 | 160 | ## License 161 | 162 | Copyright © 2016 OTTO (GmbH & Co. KG) 163 | 164 | Distributed under [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0) license. 165 | -------------------------------------------------------------------------------- /src/lambdacd_lineup/config.clj: -------------------------------------------------------------------------------- 1 | (ns lambdacd-lineup.config 2 | (:require [bouncer.core :as b] 3 | [bouncer.validators :as v] 4 | [clojure.string :as s] 5 | [clojure.tools.logging :as log]) 6 | (use [bouncer.validators :only [defvalidator]])) 7 | 8 | (defvalidator without-leading-slash? 9 | {:default-message-format "%s must not start with a slash"} 10 | [s] 11 | (or (nil? s) 12 | (= "/" s) 13 | (nil? (re-find #"^/.*$" s)))) 14 | 15 | (defvalidator with-leading-slash? 16 | {:default-message-format "%s has to start with a slash"} 17 | [s] 18 | (or (nil? s) 19 | (not (nil? (re-find #"^/.*$" s))))) 20 | 21 | (defvalidator without-trailing-slash? 22 | {:default-message-format "%s must not end with a slash"} 23 | [s] 24 | (or (nil? s) 25 | (nil? (re-find #"^.*/$" s)))) 26 | 27 | (defvalidator http-or-https? 28 | {:default-message-format "%s must be http or https"} 29 | [s] 30 | (or (nil? s) 31 | (not (nil? (re-find #"^https?.*$" s))))) 32 | 33 | (defvalidator is-vector? 34 | {:default-message-format "%s must be a vector"} 35 | [s] 36 | (or (nil? s) 37 | (instance? clojure.lang.APersistentVector s))) 38 | 39 | (defvalidator not-empty? 40 | {:default-message-format "%s must not be empty"} 41 | [s] 42 | (not (empty? s))) 43 | 44 | (defvalidator is-map? 45 | {:default-message-format "%s must be a map"} 46 | [s] 47 | (or (nil? s) 48 | (instance? clojure.lang.APersistentMap s))) 49 | 50 | (defvalidator no-duplicate-entries-in-map? 51 | {:default-message-format "%s must not have duplicate entries"} 52 | [s] 53 | (or (nil? s) 54 | (= (count (distinct (keys s))) (count (keys s))))) 55 | 56 | (defvalidator no-duplicate-entries-in-vector? 57 | {:default-message-format "%s must not have duplicate entries"} 58 | [s] 59 | (or (nil? s) 60 | (= (count (distinct s)) (count s)))) 61 | 62 | (defvalidator is-positive? 63 | {:default-message-format "%s must be a positve number"} 64 | [s] 65 | (or (nil? s) 66 | (and (number? s) (or (pos? s) (= 0.0 (float s)))))) 67 | 68 | (defvalidator is-integer? 69 | {:default-message-format "%s must be a integer"} 70 | [s] 71 | (or (nil? s) 72 | (and (number? s) (integer? s)))) 73 | 74 | (defvalidator is-float? 75 | {:default-message-format "%s must be a integer"} 76 | [s] 77 | (or (nil? s) 78 | (and (number? s) (or (integer? s) (float? s))))) 79 | 80 | (defvalidator is-boolean? 81 | {:default-message-format "%s must be a boolean"} 82 | [s] 83 | (or (nil? s) 84 | (= false s) 85 | (= true s))) 86 | 87 | (defvalidator cookie-name-is-valid? 88 | {:default-message-format "name must not be empty"} 89 | [s] 90 | (let [name (get s "name")] 91 | (or (nil? s) 92 | (and (not (nil? name)) 93 | (string? name) 94 | (not-empty name))))) 95 | 96 | (defvalidator cookie-value-is-valid? 97 | {:default-message-format "value must not be empty"} 98 | [s] 99 | (let [value (get s "value")] 100 | (or (nil? s) 101 | (and (not (nil? value)) 102 | (string? value) 103 | (not-empty value))))) 104 | 105 | (defvalidator cookie-path-is-valid? 106 | {:default-message-format "path must not be empty"} 107 | [s] 108 | (let [path (get s "path")] 109 | (or (nil? s) 110 | (nil? path) 111 | (and (string? path) 112 | (not-empty path) 113 | (or (= "/" path) 114 | (nil? (re-find #"^/.*$" path))))))) 115 | 116 | (defvalidator cookie-secure-is-valid? 117 | {:default-message-format "secure must be a boolean"} 118 | [s] 119 | (let [secure (get s "secure")] 120 | (or (nil? s) 121 | (or (nil? secure) 122 | (= false secure) 123 | (= true secure))))) 124 | 125 | (defvalidator is-firefox-or-phantomjs? 126 | {:default-message-format "%s must be \"firefox\" or \"phantomjs\""} 127 | [s] 128 | (or (nil? s) 129 | (contains? #{"firefox" "phantomjs"} s))) 130 | 131 | (defvalidator local-storage-key-is-valid? 132 | {:default-message-format "local storage key must not be empty"} 133 | [s] 134 | (let [key (key s)] 135 | (or (nil? s) 136 | (and (not (nil? key)) 137 | (string? key) 138 | (not-empty key))))) 139 | 140 | (defvalidator local-storage-value-is-valid? 141 | {:default-message-format "local storage value must not be empty"} 142 | [s] 143 | (let [value (val s)] 144 | (or (nil? s) 145 | (and (not (nil? value)) 146 | (string? value))))) 147 | 148 | (defn validate [cfg] 149 | (let [val-result (first (b/validate cfg 150 | "urls" [is-map? 151 | no-duplicate-entries-in-map? 152 | v/required 153 | [v/every #(v/string (key %))] 154 | [v/every #(http-or-https? (key %))] 155 | [v/every #(without-trailing-slash? (key %))] 156 | [v/every #(b/valid? (val %) "env-mapping" [is-map? no-duplicate-entries-in-map?])] 157 | [v/every #(b/valid? (val %) "env-mapping" [[v/every (fn [e] (clojure.core/string? (key e)))]])] 158 | [v/every #(b/valid? (val %) "env-mapping" [[v/every (fn [e] (not-empty (key e)))]])] 159 | [v/every #(b/valid? (val %) "env-mapping" [[v/every (fn [e] (clojure.core/string? (val e)))]])] 160 | [v/every #(b/valid? (val %) "cookies" [is-vector?])] 161 | [v/every #(b/valid? (val %) "cookies" [[v/every cookie-name-is-valid?]])] 162 | [v/every #(b/valid? (val %) "cookies" [[v/every cookie-value-is-valid?]])] 163 | [v/every #(b/valid? (val %) "cookies" [[v/every cookie-path-is-valid?]])] 164 | [v/every #(b/valid? (val %) "cookies" [[v/every cookie-secure-is-valid?]])] 165 | [v/every #(b/valid? (val %) "local-storage" [is-map?])] 166 | [v/every #(b/valid? (val %) "local-storage" [[v/every local-storage-key-is-valid?]])] 167 | [v/every #(b/valid? (val %) "local-storage" [[v/every local-storage-value-is-valid?]])] 168 | [v/every #(b/valid? (val %) "max-diff" [v/required is-positive? is-float?])] 169 | [v/every #(b/valid? (val %) "paths" [[v/every without-leading-slash?]])] 170 | [v/every #(b/valid? (val %) "paths" [[v/every not-empty?]])] 171 | [v/every #(b/valid? (val %) "paths" [v/required not-empty?])] 172 | [v/every #(b/valid? (val %) "resolutions" [is-vector? no-duplicate-entries-in-vector? [v/every v/number]])]] 173 | "resolutions" [is-vector? no-duplicate-entries-in-vector? [v/every v/number]] 174 | "browser" [is-firefox-or-phantomjs?] 175 | "async-wait" [is-positive? is-integer?]))] 176 | (if (nil? val-result) 177 | [true nil] 178 | [false val-result]))) -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | ============== 3 | 4 | _Version 2.0, January 2004_ 5 | _<>_ 6 | 7 | ### Terms and Conditions for use, reproduction, and distribution 8 | 9 | #### 1. Definitions 10 | 11 | “License” shall mean the terms and conditions for use, reproduction, and 12 | distribution as defined by Sections 1 through 9 of this document. 13 | 14 | “Licensor” shall mean the copyright owner or entity authorized by the copyright 15 | owner that is granting the License. 16 | 17 | “Legal Entity” shall mean the union of the acting entity and all other entities 18 | that control, are controlled by, or are under common control with that entity. 19 | For the purposes of this definition, “control” means **(i)** the power, direct or 20 | indirect, to cause the direction or management of such entity, whether by 21 | contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the 22 | outstanding shares, or **(iii)** beneficial ownership of such entity. 23 | 24 | “You” (or “Your”) shall mean an individual or Legal Entity exercising 25 | permissions granted by this License. 26 | 27 | “Source” form shall mean the preferred form for making modifications, including 28 | but not limited to software source code, documentation source, and configuration 29 | files. 30 | 31 | “Object” form shall mean any form resulting from mechanical transformation or 32 | translation of a Source form, including but not limited to compiled object code, 33 | generated documentation, and conversions to other media types. 34 | 35 | “Work” shall mean the work of authorship, whether in Source or Object form, made 36 | available under the License, as indicated by a copyright notice that is included 37 | in or attached to the work (an example is provided in the Appendix below). 38 | 39 | “Derivative Works” shall mean any work, whether in Source or Object form, that 40 | is based on (or derived from) the Work and for which the editorial revisions, 41 | annotations, elaborations, or other modifications represent, as a whole, an 42 | original work of authorship. For the purposes of this License, Derivative Works 43 | shall not include works that remain separable from, or merely link (or bind by 44 | name) to the interfaces of, the Work and Derivative Works thereof. 45 | 46 | “Contribution” shall mean any work of authorship, including the original version 47 | of the Work and any modifications or additions to that Work or Derivative Works 48 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 49 | by the copyright owner or by an individual or Legal Entity authorized to submit 50 | on behalf of the copyright owner. For the purposes of this definition, 51 | “submitted” means any form of electronic, verbal, or written communication sent 52 | to the Licensor or its representatives, including but not limited to 53 | communication on electronic mailing lists, source code control systems, and 54 | issue tracking systems that are managed by, or on behalf of, the Licensor for 55 | the purpose of discussing and improving the Work, but excluding communication 56 | that is conspicuously marked or otherwise designated in writing by the copyright 57 | owner as “Not a Contribution.” 58 | 59 | “Contributor” shall mean Licensor and any individual or Legal Entity on behalf 60 | of whom a Contribution has been received by Licensor and subsequently 61 | incorporated within the Work. 62 | 63 | #### 2. Grant of Copyright License 64 | 65 | Subject to the terms and conditions of this License, each Contributor hereby 66 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 67 | irrevocable copyright license to reproduce, prepare Derivative Works of, 68 | publicly display, publicly perform, sublicense, and distribute the Work and such 69 | Derivative Works in Source or Object form. 70 | 71 | #### 3. Grant of Patent License 72 | 73 | Subject to the terms and conditions of this License, each Contributor hereby 74 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 75 | irrevocable (except as stated in this section) patent license to make, have 76 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 77 | such license applies only to those patent claims licensable by such Contributor 78 | that are necessarily infringed by their Contribution(s) alone or by combination 79 | of their Contribution(s) with the Work to which such Contribution(s) was 80 | submitted. If You institute patent litigation against any entity (including a 81 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 82 | Contribution incorporated within the Work constitutes direct or contributory 83 | patent infringement, then any patent licenses granted to You under this License 84 | for that Work shall terminate as of the date such litigation is filed. 85 | 86 | #### 4. Redistribution 87 | 88 | You may reproduce and distribute copies of the Work or Derivative Works thereof 89 | in any medium, with or without modifications, and in Source or Object form, 90 | provided that You meet the following conditions: 91 | 92 | * **(a)** You must give any other recipients of the Work or Derivative Works a copy of 93 | this License; and 94 | * **(b)** You must cause any modified files to carry prominent notices stating that You 95 | changed the files; and 96 | * **(c)** You must retain, in the Source form of any Derivative Works that You distribute, 97 | all copyright, patent, trademark, and attribution notices from the Source form 98 | of the Work, excluding those notices that do not pertain to any part of the 99 | Derivative Works; and 100 | * **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any 101 | Derivative Works that You distribute must include a readable copy of the 102 | attribution notices contained within such NOTICE file, excluding those notices 103 | that do not pertain to any part of the Derivative Works, in at least one of the 104 | following places: within a NOTICE text file distributed as part of the 105 | Derivative Works; within the Source form or documentation, if provided along 106 | with the Derivative Works; or, within a display generated by the Derivative 107 | Works, if and wherever such third-party notices normally appear. The contents of 108 | the NOTICE file are for informational purposes only and do not modify the 109 | License. You may add Your own attribution notices within Derivative Works that 110 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 111 | provided that such additional attribution notices cannot be construed as 112 | modifying the License. 113 | 114 | You may add Your own copyright statement to Your modifications and may provide 115 | additional or different license terms and conditions for use, reproduction, or 116 | distribution of Your modifications, or for any such Derivative Works as a whole, 117 | provided Your use, reproduction, and distribution of the Work otherwise complies 118 | with the conditions stated in this License. 119 | 120 | #### 5. Submission of Contributions 121 | 122 | Unless You explicitly state otherwise, any Contribution intentionally submitted 123 | for inclusion in the Work by You to the Licensor shall be under the terms and 124 | conditions of this License, without any additional terms or conditions. 125 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 126 | any separate license agreement you may have executed with Licensor regarding 127 | such Contributions. 128 | 129 | #### 6. Trademarks 130 | 131 | This License does not grant permission to use the trade names, trademarks, 132 | service marks, or product names of the Licensor, except as required for 133 | reasonable and customary use in describing the origin of the Work and 134 | reproducing the content of the NOTICE file. 135 | 136 | #### 7. Disclaimer of Warranty 137 | 138 | Unless required by applicable law or agreed to in writing, Licensor provides the 139 | Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, 140 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 141 | including, without limitation, any warranties or conditions of TITLE, 142 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 143 | solely responsible for determining the appropriateness of using or 144 | redistributing the Work and assume any risks associated with Your exercise of 145 | permissions under this License. 146 | 147 | #### 8. Limitation of Liability 148 | 149 | In no event and under no legal theory, whether in tort (including negligence), 150 | contract, or otherwise, unless required by applicable law (such as deliberate 151 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 152 | liable to You for damages, including any direct, indirect, special, incidental, 153 | or consequential damages of any character arising as a result of this License or 154 | out of the use or inability to use the Work (including but not limited to 155 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 156 | any and all other commercial damages or losses), even if such Contributor has 157 | been advised of the possibility of such damages. 158 | 159 | #### 9. Accepting Warranty or Additional Liability 160 | 161 | While redistributing the Work or Derivative Works thereof, You may choose to 162 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 163 | other liability obligations and/or rights consistent with this License. However, 164 | in accepting such obligations, You may act only on Your own behalf and on Your 165 | sole responsibility, not on behalf of any other Contributor, and only if You 166 | agree to indemnify, defend, and hold each Contributor harmless for any liability 167 | incurred by, or claims asserted against, such Contributor by reason of your 168 | accepting any such warranty or additional liability. 169 | 170 | _END OF TERMS AND CONDITIONS_ 171 | 172 | ### APPENDIX: How to apply the Apache License to your work 173 | 174 | To apply the Apache License to your work, attach the following boilerplate 175 | notice, with the fields enclosed by brackets `[]` replaced with your own 176 | identifying information. (Don't include the brackets!) The text should be 177 | enclosed in the appropriate comment syntax for the file format. We also 178 | recommend that a file or class name and description of purpose be included on 179 | the same “printed page” as the copyright notice for easier identification within 180 | third-party archives. 181 | 182 | Copyright [yyyy] [name of copyright owner] 183 | 184 | Licensed under the Apache License, Version 2.0 (the "License"); 185 | you may not use this file except in compliance with the License. 186 | You may obtain a copy of the License at 187 | 188 | http://www.apache.org/licenses/LICENSE-2.0 189 | 190 | Unless required by applicable law or agreed to in writing, software 191 | distributed under the License is distributed on an "AS IS" BASIS, 192 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 193 | See the License for the specific language governing permissions and 194 | limitations under the License. 195 | 196 | -------------------------------------------------------------------------------- /src/lambdacd_lineup/core.clj: -------------------------------------------------------------------------------- 1 | (ns lambdacd-lineup.core 2 | (:require [lambdacd.steps.shell :as shell] 3 | [lambdacd-artifacts.core :as artifacts] 4 | [cheshire.core :as cheshire] 5 | [lambdacd-lineup.config :as config] 6 | [lambdacd-lineup.util :as util] 7 | [lambdacd-lineup.io :as io] 8 | [clojure.string :as s] 9 | [clojure.core.strint :as strint] 10 | [clj-http.client :as client] 11 | [lambdacd.steps.support :refer [new-printer print-to-output printed-output]])) 12 | 13 | 14 | (defn check-status-code-for-one-url [url-with-path] 15 | (let [status (:status (client/get url-with-path {:throw-exceptions false :ignore-unknown-host? true}))] 16 | (if (not (or (nil? status) (> status 400))) 17 | [] 18 | [url-with-path]))) 19 | 20 | (defn check-status-code [url paths] 21 | (let [urls-with-paths (map #(util/concat-url-and-path url %1) paths)] 22 | (reduce (fn [old url-with-path] 23 | (concat old (check-status-code-for-one-url url-with-path))) 24 | [] urls-with-paths))) 25 | 26 | (defn take-screenshots-output [ctx printer url paths resolutions cookies localStorage] 27 | (print-to-output ctx printer (str "URL: " url)) 28 | (print-to-output ctx printer (str "Paths: " paths)) 29 | (print-to-output ctx printer (str "Resolutions: " resolutions)) 30 | (print-to-output ctx printer (str "Cookies: " cookies)) 31 | (print-to-output ctx printer (str "LocalStorage: " localStorage)) 32 | (print-to-output ctx printer "") 33 | (print-to-output ctx printer "-------------------------------------------------") 34 | (print-to-output ctx printer "")) 35 | 36 | (defn invalid-status-code-output [ctx printer url paths failed-paths] 37 | (print-to-output ctx printer (str "URL: " url)) 38 | (print-to-output ctx printer (str "Paths: " paths)) 39 | (print-to-output ctx printer "") 40 | (print-to-output ctx printer (str "Can't connect to " (s/join ", " failed-paths))) 41 | (print-to-output ctx printer "-------------------------------------------------") 42 | (print-to-output ctx printer "")) 43 | 44 | (defn get-domain-from-url [url] 45 | (let [match (re-matches #"(?:[^:/]*:)?([^.]*\.[^.]*\.|[^/]*).*" (clojure.string/reverse url))] 46 | (if (nil? match) 47 | "can-not-extract-domain" 48 | (clojure.string/reverse (second match))))) 49 | 50 | (defn set-cookie-defaults [url cookie] 51 | {:name (get cookie "name") 52 | :value (get cookie "value") 53 | :secure (or (get cookie "secure") false) 54 | :path (str "/" (or (get cookie "path") "")) 55 | :domain (get-domain-from-url url) 56 | }) 57 | 58 | 59 | (defn take-screenshot [ctx home-dir script url resolutions paths dir phantomjs? async-wait cookies local-storage] 60 | (shell/bash 61 | ctx 62 | home-dir 63 | (strint/<< "ruby lineup/~{script} \"~{url}\" \"~{resolutions}\" \"~{paths}\" \"~{dir}\" \"~{phantomjs?}\" \"~{async-wait}\" '~{cookies}' '~{local-storage}'")) 64 | ) 65 | 66 | (defn interate-urls-to-take-screenshots 67 | ([lineup-cfg script-name build-number env ctx home-dir printer] 68 | (interate-urls-to-take-screenshots (get lineup-cfg "urls") lineup-cfg script-name build-number env ctx home-dir printer :success)) 69 | ([urls lineup-cfg script-name build-number env ctx home-dir printer status] 70 | 71 | (if (or (empty? urls) (= :failure status)) 72 | {:status status} 73 | (let [url-configuration (val (first urls)) 74 | env-mapping (get url-configuration "env-mapping") 75 | url (util/replace-env-in-url (key (first urls)) env env-mapping) 76 | url-for-dir (util/replace-special-chars-in-url url) 77 | paths (or (get url-configuration "paths") "/") 78 | paths-as-string (s/join "," paths) 79 | cookies (or (get url-configuration "cookies") []) 80 | cookies-with-defaults (map (partial set-cookie-defaults url) cookies) 81 | cookies-as-json (cheshire/generate-string cookies-with-defaults) 82 | local-storage (or (get url-configuration "local-storage") {}) 83 | local-storage-as-json (cheshire/generate-string local-storage) 84 | resolutions (or (get url-configuration "resolutions") (get lineup-cfg "resolutions") [(str 1200)]) 85 | resolutions-as-string (s/join "," resolutions) 86 | async-wait (str (or (get lineup-cfg "async-wait") 5)) 87 | browser (or (get lineup-cfg "browser") "firefox") 88 | phantomjs? (= "phantomjs" browser) 89 | dir (str home-dir "/screenshots/" build-number "-" url-for-dir) 90 | invalid-paths-list (check-status-code url paths)] 91 | (if (empty? invalid-paths-list) 92 | (do 93 | (take-screenshots-output ctx printer url paths-as-string resolutions-as-string cookies-as-json local-storage-as-json) 94 | (recur (rest urls) 95 | lineup-cfg 96 | script-name 97 | build-number 98 | env 99 | ctx 100 | home-dir 101 | printer 102 | (:status (take-screenshot ctx home-dir script-name url resolutions-as-string paths-as-string dir phantomjs? async-wait cookies-as-json local-storage-as-json)))) 103 | (do 104 | (invalid-status-code-output ctx printer url paths-as-string invalid-paths-list) 105 | {:status :failure})))))) 106 | 107 | (defn validation-output [ctx printer validation-result] 108 | (print-to-output ctx printer "Configuration Validation Error") 109 | (print-to-output ctx printer (second validation-result))) 110 | 111 | (defn execute-lineup-script [env printer script-name] 112 | (fn [_ {build-number :build-number {home-dir :home-dir lineup-cfg :lineup-cfg} :config :as ctx}] 113 | (let [validation-result (config/validate lineup-cfg)] 114 | (if (not (first validation-result)) 115 | (do 116 | (validation-output ctx printer validation-result) 117 | {:status :failure}) 118 | (let [lineup-folder (io/ensure-dir home-dir "lineup")] 119 | (io/copy-to lineup-folder script-name) 120 | (interate-urls-to-take-screenshots lineup-cfg 121 | script-name 122 | build-number 123 | env 124 | ctx 125 | home-dir 126 | printer)))))) 127 | 128 | (defn take-screenshots 129 | {:meta-step true} 130 | ([] (take-screenshots "www")) 131 | ([env] 132 | (let [printer (new-printer)] 133 | (execute-lineup-script env printer "lineup_screenshot.rb")))) 134 | 135 | (defn compare-with-screenshots 136 | {:meta-step true} 137 | ([] (compare-with-screenshots "www")) 138 | ([env] 139 | (let [printer (new-printer)] 140 | (execute-lineup-script env printer "lineup_compare.rb")))) 141 | 142 | (defn copy-files-output [ctx printer] 143 | (print-to-output ctx printer "Error: Can't rename files.")) 144 | 145 | (defn rename-and-publish-lineup-files [args {build-number :build-number {home-dir :home-dir lineup-cfg :lineup-cfg} :config :as ctx} env printer] 146 | (loop [urls (get lineup-cfg "urls") 147 | result {:status :success :details []} 148 | artifacts-list '()] 149 | (if (or (empty? urls) (= :failure (:status result))) 150 | (assoc result :details [{:label "Artifacts", :details artifacts-list}]) 151 | (let [env-mapping (get (val (first urls)) "env-mapping") 152 | url (util/replace-env-in-url (key (first urls)) env env-mapping) 153 | url-for-dir (util/replace-special-chars-in-url url) 154 | dir (str home-dir "/screenshots/" build-number "-" url-for-dir) 155 | shell-result (shell/bash ctx dir (str "for f in * ; do mv \"$f\" \"" url-for-dir "_$f\" ; done"))] 156 | (if (= :failure (:status shell-result)) 157 | (do (copy-files-output ctx printer) 158 | {:status :failure}) 159 | (let [new-artifacts-list (:details (first (:details (artifacts/publish-artifacts 160 | args 161 | ctx 162 | dir [#".*"])))) 163 | result-with-details (update-in result [:details] #(concat % artifacts-list))] 164 | (recur (rest urls) result-with-details (concat artifacts-list new-artifacts-list)))))))) 165 | 166 | (defn calc-detected-max-diff [json-result] 167 | (if (empty? json-result) 168 | 0 169 | (apply max (map :difference json-result)))) 170 | 171 | (defn calc-new-status [old-result max-detected-diff max-diff] 172 | (let [new-status (if (<= max-detected-diff max-diff) :success :failure) 173 | old-status (:status old-result) 174 | combined-with-old-status (if (= :failure old-status) old-status new-status)] 175 | {:status combined-with-old-status})) 176 | 177 | (defn analyse-output [ctx printer url max-detected-diff max-diff] 178 | (print-to-output ctx printer (str "URL: " url)) 179 | (print-to-output ctx printer (str "actual max difference: " max-detected-diff " %")) 180 | (print-to-output ctx printer (str "target max difference: " max-diff " %")) 181 | (print-to-output ctx printer (str "result: " (if (<= max-detected-diff max-diff) "Success" "Failure"))) 182 | (print-to-output ctx printer "") 183 | (print-to-output ctx printer "-------------------------------------------------") 184 | (print-to-output ctx printer "")) 185 | 186 | (defn error-output [ctx printer url url-for-dir] 187 | (print-to-output ctx printer (str "URL: " url)) 188 | (print-to-output ctx printer (str "Can't find " url-for-dir "_log.json")) 189 | (print-to-output ctx printer "") 190 | (print-to-output ctx printer "-------------------------------------------------") 191 | (print-to-output ctx printer "")) 192 | 193 | (defn iterate-and-analyse-logs [{build-number :build-number step-id :step-id {home-dir :home-dir lineup-cfg :lineup-cfg} :config :as ctx} env printer] 194 | (loop [urls (get lineup-cfg "urls") 195 | result {:status :success}] 196 | (if (empty? urls) 197 | result 198 | (let [dir (str home-dir "/" build-number "/" (artifacts/format-step-id step-id)) 199 | env-mapping (get (val (first urls)) "env-mapping") 200 | url (util/replace-env-in-url (key (first urls)) env env-mapping) 201 | url-for-dir (util/replace-special-chars-in-url url) 202 | max-diff (get (val (first urls)) "max-diff")] 203 | (if (io/lineup-json-exists url home-dir build-number step-id) 204 | (let [json-result (cheshire/parse-stream (clojure.java.io/reader (str dir "/" url-for-dir "_log.json")) true) 205 | max-detected-diff (calc-detected-max-diff json-result) 206 | new-result (calc-new-status result max-detected-diff max-diff)] 207 | (analyse-output ctx printer url max-detected-diff max-diff) 208 | (recur (rest urls) new-result)) 209 | (do (error-output ctx printer url url-for-dir) 210 | (recur (rest urls) {:status :failure}))))))) 211 | 212 | (defn analyse-comparison 213 | {:meta-step true} 214 | ([] 215 | (analyse-comparison "www")) 216 | ([env] 217 | (fn [args ctx] 218 | (let [printer (new-printer) 219 | result (rename-and-publish-lineup-files args ctx env printer)] 220 | (if (= :success (:status result)) 221 | (assoc (iterate-and-analyse-logs ctx env printer) :details (:details result)) 222 | result))))) 223 | 224 | -------------------------------------------------------------------------------- /test/lambdacd_lineup/config_test.clj: -------------------------------------------------------------------------------- 1 | (ns lambdacd-lineup.config-test 2 | (:require [clojure.test :refer :all] 3 | [lambdacd-lineup.config :refer :all])) 4 | 5 | 6 | (deftest validate-test 7 | (testing "valid minimal config" 8 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5}}}] 9 | (is (first (validate cfg))))) 10 | (testing "urls: urls is required" 11 | (let [cfg {"resolutions" [800, 1200]}] 12 | (is (not (first (validate cfg)))))) 13 | (testing "urls: leading slash is invalid" 14 | (let [cfg {"urls" {"/http://otto.de" {"paths" ["/"] "max-diff" 5}}}] 15 | (is (not (first (validate cfg)))))) 16 | (testing "urls: leading slash is invalid" 17 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5} "/peter.de" {"paths" ["/"] "max-diff" 5}}}] 18 | (is (not (first (validate cfg)))))) 19 | (testing "urls: trailing slash is invalid" 20 | (let [cfg {"urls" {"http://otto.de/" {"paths" ["/"] "max-diff" 5}}}] 21 | (is (not (first (validate cfg)))))) 22 | (testing "urls: invalid empty string" 23 | (let [cfg {"urls" {"" {"paths" ["/"] "max-diff" 5}}}] 24 | (is (not (first (validate cfg)))))) 25 | (testing "urls: invalid keyword as key" 26 | (let [cfg {"urls" {:mypath {"paths" ["/"] "max-diff" 5}}}] 27 | (is (not (first (validate cfg)))))) 28 | (testing "urls: one valid url and one invalid empty url" 29 | (let [cfg {"urls" {"http://www.otto.de" {"paths" ["/"] "max-diff" 5} "" {"paths" ["/"] "max-diff" 5}}}] 30 | (is (not (first (validate cfg)))))) 31 | (testing "urls: as vector is invalid" 32 | (let [cfg {"urls" ["http://otto.de" {"paths" ["/"] "max-diff" 5} "http://otto.de" {"paths" ["/"] "max-diff" 5}]}] 33 | (is (not (first (validate cfg)))))) 34 | (testing "urls: wrong protrocol" 35 | (let [cfg {"urls" ["ftp://otto.de" {"paths" ["/"] "max-diff" 5}]}] 36 | (is (not (first (validate cfg)))))) 37 | 38 | (testing "paths: two valid paths - 1" 39 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/" "sport"] "max-diff" 5}}}] 40 | (is (first (validate cfg))))) 41 | (testing "paths: two valid paths - 2" 42 | (let [cfg {"urls" {"http://otto.de" {"paths" ["multimedia" "sport"] "max-diff" 5}}}] 43 | (is (first (validate cfg))))) 44 | (testing "paths: key is required" 45 | (let [cfg {"urls" {"http://otto.de" {"max-diff" 5}}}] 46 | (is (not (first (validate cfg)))))) 47 | (testing "paths: invalid empty path string" 48 | (let [cfg {"urls" {"http://otto.de" {"paths" [""] "max-diff" 5}}}] 49 | (is (not (first (validate cfg)))))) 50 | (testing "paths: invalid empty path" 51 | (let [cfg {"urls" {"http://otto.de" {"paths" [] "max-diff" 5}}}] 52 | (is (not (first (validate cfg)))))) 53 | (testing "paths: one valid path and one invalid empty path" 54 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/", ""] "max-diff" 5}}}] 55 | (is (not (first (validate cfg)))))) 56 | (testing "paths: a list is invalid" 57 | (let [cfg {"urls" {"http://otto.de" {"paths" '("/", "") "max-diff" 5}}}] 58 | (is (not (first (validate cfg)))))) 59 | (testing "paths: with leading slash is invalid" 60 | (let [cfg {"urls" {"http://otto.de" {"paths" ["sport", "/media"] "max-diff" 5}}}] 61 | (is (not (first (validate cfg)))))) 62 | 63 | (testing "env-mapping: valid" 64 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/", "sport"] "max-diff" 5 "env-mapping" {"live" "www"}}}}] 65 | (is (first (validate cfg))))) 66 | (testing "env-mapping: valid two mappings" 67 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/", "sport"] "max-diff" 5 "env-mapping" {"live" "www" 68 | "develop" "dev"}}}}] 69 | (is (first (validate cfg))))) 70 | (testing "env-mapping: invalid vector" 71 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/", "sport"] "max-diff" 5 "env-mapping" ["live" "www"]}}}] 72 | (is (not (first (validate cfg)))))) 73 | (testing "env-mapping: key must be a string" 74 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/", "sport"] "max-diff" 5 "env-mapping" {:live "www"}}}}] 75 | (is (not (first (validate cfg)))))) 76 | (testing "env-mapping: key must not be empty" 77 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/", "sport"] "max-diff" 5 "env-mapping" {"" "www"}}}}] 78 | (is (not (first (validate cfg)))))) 79 | (testing "env-mapping: value must be a string" 80 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/", "sport"] "max-diff" 5 "env-mapping" {"live" :www}}}}] 81 | (is (not (first (validate cfg)))))) 82 | 83 | (testing "cookies: valid map" 84 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] 85 | "max-diff" 5 86 | "cookies" [{"name" "myCookieName" 87 | "value" "myCookieValue" 88 | "secure" true}]}}}] 89 | (is (first (validate cfg))))) 90 | (testing "cookies: cookie must be a vector" 91 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] 92 | "max-diff" 5 93 | "cookies" {"name" "myCookieName" 94 | "value" "myCookieValue"}}}}] 95 | (is (not (first (validate cfg)))))) 96 | (testing "cookies: all cookies must be verified" 97 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] 98 | "max-diff" 5 99 | "cookies" [{"name" "myCookieName" 100 | "value" "myCookieValue"} 101 | {"name" "myCookieName"}]}}}] 102 | (is (not (first (validate cfg)))))) 103 | (testing "cookies: two valid cookies" 104 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] 105 | "max-diff" 5 106 | "cookies" [{"name" "myCookieName" 107 | "value" "myCookieValue"} 108 | {"name" "myCookieName" 109 | "value" "myCookieValue"}]}}}] 110 | (is (first (validate cfg))))) 111 | (testing "cookies: name is required" 112 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] 113 | "max-diff" 5 114 | "cookies" [{"value" "myCookieValue"}]}}}] 115 | (is (not (first (validate cfg)))))) 116 | 117 | (testing "cookies: vector can be empty" 118 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] 119 | "max-diff" 5 120 | "cookies" []}}}] 121 | (is (first (validate cfg))))) 122 | 123 | (testing "cookies: value is required" 124 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] 125 | "max-diff" 5 126 | "cookies" [{"name" "myCookieName"}]}}}] 127 | (is (not (first (validate cfg)))))) 128 | (testing "cookies: name must be a string" 129 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] 130 | "max-diff" 5 131 | "cookies" [{"name" 123 "value" "myCookieValue"}]}}}] 132 | (is (not (first (validate cfg)))))) 133 | (testing "cookies: value must be a string" 134 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] 135 | "max-diff" 5 136 | "cookies" [{"name" "myCookieName" "value" 123}]}}}] 137 | (is (not (first (validate cfg)))))) 138 | (testing "cookies: name must not be empty" 139 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] 140 | "max-diff" 5 141 | "cookies" [{"name" "" "value" "myCookieValue"}]}}}] 142 | (is (not (first (validate cfg)))))) 143 | (testing "cookies: value must not be empty" 144 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] 145 | "max-diff" 5 146 | "cookies" [{"name" "myCookieName" "value" ""}]}}}] 147 | (is (not (first (validate cfg)))))) 148 | (testing "cookies: path must be a string" 149 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] 150 | "max-diff" 5 151 | "cookies" [{"name" "myCookieName" "value" "myCookieValue" "path" 123}]}}}] 152 | (is (not (first (validate cfg)))))) 153 | (testing "cookies: valid root path" 154 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] 155 | "max-diff" 5 156 | "cookies" [{"name" "myCookieName" "value" "myCookieValue" "path" "/"}]}}}] 157 | (is (first (validate cfg))))) 158 | (testing "cookies: valid path" 159 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] 160 | "max-diff" 5 161 | "cookies" [{"name" "myCookieName" "value" "myCookieValue" "path" "subpath"}]}}}] 162 | (is (first (validate cfg))))) 163 | (testing "cookies: invalid path. it starts with a slash" 164 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] 165 | "max-diff" 5 166 | "cookies" [{"name" "myCookieName" "value" "myCookieValue" "path" "/subpath"}]}}}] 167 | (is (not (first (validate cfg)))))) 168 | (testing "cookies: path must not be empty" 169 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] 170 | "max-diff" 5 171 | "cookies" [{"name" "myCookieName" "value" "myCookieValue" "path" ""}]}}}] 172 | (is (not (first (validate cfg)))))) 173 | (testing "cookies: secure must be a boolean" 174 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] 175 | "max-diff" 5 176 | "cookies" [{"name" "myCookieName" "value" "myCookieValue" "secure" "true"}]}}}] 177 | (is (not (first (validate cfg)))))) 178 | 179 | (testing "local-storage: valid map" 180 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] 181 | "max-diff" 5 182 | "local-storage" {"key" "value"}}}}] 183 | (is (first (validate cfg))))) 184 | (testing "local-storage: two valid entries" 185 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] 186 | "max-diff" 5 187 | "local-storage" {"key1" "value1" "key2" "value2"}}}}] 188 | (is (first (validate cfg))))) 189 | 190 | (testing "local-storage: must be a map" 191 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] 192 | "max-diff" 5 193 | "local-storage" ["value1"]}}}] 194 | (is (not (first (validate cfg)))))) 195 | (testing "local-storage: map can be empty" 196 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] 197 | "max-diff" 5 198 | "local-storage" {}}}}] 199 | (is (first (validate cfg))))) 200 | (testing "local-storage: all entries must be verified" 201 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] 202 | "max-diff" 5 203 | "local-storage" {"key1" "value1" "key" 100}}}}] 204 | (is (not (first (validate cfg)))))) 205 | 206 | (testing "local-storage: key must be a string" 207 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] 208 | "max-diff" 5 209 | "local-storage" {100 "value1"}}}}] 210 | (is (not (first (validate cfg)))))) 211 | (testing "local-storage: key must not be empty" 212 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] 213 | "max-diff" 5 214 | "local-storage" {"" "value1"}}}}] 215 | (is (not (first (validate cfg)))))) 216 | (testing "local-storage: value must be a string" 217 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] 218 | "max-diff" 5 219 | "local-storage" {"key" 100}}}}] 220 | (is (not (first (validate cfg)))))) 221 | (testing "local-storage: value can be empty" 222 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] 223 | "max-diff" 5 224 | "local-storage" {"key1" ""}}}}] 225 | (is (first (validate cfg))))) 226 | 227 | (testing "max-diff: valid value" 228 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5}}}] 229 | (is (first (validate cfg))))) 230 | (testing "max-diff: key is required" 231 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"]}}}] 232 | (is (not (first (validate cfg)))))) 233 | (testing "max-diff: invalid negative value" 234 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" -5}}}] 235 | (is (not (first (validate cfg)))))) 236 | (testing "max-diff: valid positive float value" 237 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5.1}}}] 238 | (is (first (validate cfg))))) 239 | (testing "max-diff: invalid string value" 240 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" "5"}}}] 241 | (is (not (first (validate cfg)))))) 242 | (testing "max-diff: invalid vector value" 243 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" [5]}}}] 244 | (is (not (first (validate cfg)))))) 245 | 246 | (testing "resolutions: valid empty vector" 247 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5}} "resolutions" []}] 248 | (is (first (validate cfg))))) 249 | (testing "resolutions: valid vector with two resolutions" 250 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5}} "resolutions" [800, 1200]}] 251 | (is (first (validate cfg))))) 252 | (testing "resolutions: only a string is invalid" 253 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5}} "resolutions" "1200"}] 254 | (is (not (first (validate cfg)))))) 255 | (testing "resolutions: vector with string is invalid" 256 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5}} "resolutions" ["1200"]}] 257 | (is (not (first (validate cfg)))))) 258 | (testing "resolutions: vector with a number and a string is invalid" 259 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5}} "resolutions" [800, "1200"]}] 260 | (is (not (first (validate cfg)))))) 261 | (testing "resolutions: only a number invalid" 262 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5}} "resolutions" 1200}] 263 | (is (not (first (validate cfg)))))) 264 | (testing "resolutions: a list is invalid" 265 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5}} "resolutions" '(1200)}] 266 | (is (not (first (validate cfg)))))) 267 | (testing "resolutions: same resolution twice is invalid" 268 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5}} "resolutions" [800, 1200, 800]}] 269 | (is (not (first (validate cfg)))))) 270 | 271 | (testing "urls.resolutions: valid empty vector" 272 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5 "resolutions" []}}}] 273 | (is (first (validate cfg))))) 274 | (testing "urls.resolutions: valid vector with two resolutions" 275 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5 "resolutions" [800, 1200]}}}] 276 | (is (first (validate cfg))))) 277 | (testing "urls.resolutions: only a string is invalid" 278 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5 "resolutions" "1200"}}}] 279 | (is (not (first (validate cfg)))))) 280 | (testing "urls.resolutions: vector with string is invalid" 281 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5 "resolutions" ["1200"]}}}] 282 | (is (not (first (validate cfg)))))) 283 | (testing "urls.resolutions: vector with a number and a string is invalid" 284 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5 "resolutions" [800, "1200"]}}}] 285 | (is (not (first (validate cfg)))))) 286 | (testing "urls.resolutions: only a number invalid" 287 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5 "resolutions" 1200}}}] 288 | (is (not (first (validate cfg)))))) 289 | (testing "urls.resolutions: a list is invalid" 290 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5 "resolutions" '(1200)}}}] 291 | (is (not (first (validate cfg)))))) 292 | (testing "urls.resolutions: same resolution twice is invalid" 293 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5 "resolutions" [800, 1200, 800]}}}] 294 | (is (not (first (validate cfg)))))) 295 | 296 | (testing "browser: valid firefox" 297 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5}} 298 | "browser" "firefox"}] 299 | (is (first (validate cfg))))) 300 | (testing "browser: valid phantomjs" 301 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5}} 302 | "browser" "phantomjs"}] 303 | (is (first (validate cfg))))) 304 | (testing "browser: invalid string" 305 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5}} 306 | "browser" "chrome"}] 307 | (is (not (first (validate cfg)))))) 308 | (testing "browser: keyword is invalid" 309 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5}} 310 | "browser" :firefox}] 311 | (is (not (first (validate cfg)))))) 312 | 313 | (testing "async-wait: valid value" 314 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5}} 315 | "async-wait" 3}] 316 | (is (first (validate cfg))))) 317 | (testing "async-wait: invalid negative value" 318 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5}} 319 | "async-wait" -3}] 320 | (is (not (first (validate cfg)))))) 321 | (testing "async-wait: invalid positive float value" 322 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5}} 323 | "async-wait" 4.3}] 324 | (is (not (first (validate cfg)))))) 325 | (testing "async-wait: invalid string value" 326 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5}} 327 | "async-wait" "4"}] 328 | (is (not (first (validate cfg)))))) 329 | (testing "async-wait: invalid vector value" 330 | (let [cfg {"urls" {"http://otto.de" {"paths" ["/"] "max-diff" 5}} 331 | "async-wait" [4, 5]}] 332 | (is (not (first (validate cfg))))))) --------------------------------------------------------------------------------