├── 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 | [](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 | 
157 |
158 | 
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)))))))
--------------------------------------------------------------------------------