vivisector 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: cd1749784defc5a1e9d266f8facd3423f42b4a5e
4
+ data.tar.gz: ca695ab523abff57b7fd7f2d1673c2b3579245ff
5
+ SHA512:
6
+ metadata.gz: 6f89aa97f267fc8552d9be9aa2a993a90f896636be4d1831d21fba09ed78b9080146da12c2fa6629c35b3712c0560a07a95bdc96e1ca4697efaeb3029842d31e
7
+ data.tar.gz: c02b5c7656672f41bbde11059a0f393e786ed249bd68fc30b189ba0d33ad2d37ba2f9d3b595ca05607c2d4cb7789ecdac66d07c5d9b6b82f91dbca3ce94b0e11
@@ -0,0 +1,15 @@
1
+
2
+ # Vivisector exceptions
3
+
4
+ module Vivisector
5
+
6
+ # For enforcing the use of certain classes even though they may share a base class
7
+ class IncompatibilityError < TypeError; end
8
+
9
+ # For reporting improper extension of the Vivisector base classes (developer error)
10
+ class ImplementationError < NotImplementedError; end
11
+
12
+ # For reporting assertions that fail at runtime (logic errors in extender's code)
13
+ class OperationalError < RuntimeError; end
14
+
15
+ end
@@ -0,0 +1,22 @@
1
+ require 'pathname'
2
+ require_relative 'exceptions'
3
+
4
+ # return true if the other_path is deeper than or equal to the base path
5
+ # http://stackoverflow.com/a/26878510/2063546
6
+ def ancestor?(base, other_path)
7
+ base_parts = File.expand_path(base).split('/')
8
+ path_parts = File.expand_path(other_path).split('/')
9
+ path_parts[0..base_parts.size-1] == base_parts
10
+ end
11
+
12
+ # return the relative path from a document in a web root to a media element, given full paths to each
13
+ def web_relative_path(web_root, base_document, child_element)
14
+ c = File.expand_path(child_element)
15
+ r = File.expand_path(web_root)
16
+ unless ancestor?(r, c)
17
+ raise Vivisector::OperationalError, "Child element '#{c}' is not an ancestor of the web root '#{r}'"
18
+ end
19
+ base = Pathname.new (File.dirname(File.expand_path(base_document)))
20
+ elem = Pathname.new c
21
+ (elem.relative_path_from base).to_s
22
+ end
@@ -0,0 +1,19 @@
1
+
2
+ module Vivisector
3
+
4
+ # A simple container for image information
5
+ class Image
6
+ attr_accessor :path # the location on disk
7
+ attr_accessor :id # an application-specific unique identifier
8
+ attr_accessor :description # a friendly description
9
+ end
10
+
11
+
12
+ # Design images may optionally specify an attribute
13
+ class DesignImage < Image
14
+ attr_accessor :attribute
15
+ end
16
+
17
+ # App images are the same as images, but create this alias for symmetry / clarity
18
+ class AppImage < Image; end
19
+ end
@@ -0,0 +1,60 @@
1
+
2
+ class ImageLoader
3
+
4
+ attr_reader :image_set
5
+
6
+ def initialize
7
+ @image_set = nil # the image set for this loader
8
+
9
+ end
10
+
11
+ def load
12
+ image_set = self.fetch_image_set
13
+ self.allow_image_set image_set
14
+ @image_set = image_set
15
+ end
16
+
17
+ # implementation-specific: return a filled ImageSet
18
+ def fetch_image_set
19
+ puts " +++ If you're seeing this, #{self.class.name}.#{__method__} was not overridden"
20
+ end
21
+
22
+ # implementation-specific: verify that an ImageSet is appropriate
23
+ def allow_image_set image_set
24
+ puts " +++ If you're seeing this, #{self.class.name}.#{__method__} was not overridden"
25
+ end
26
+
27
+ end
28
+
29
+
30
+ module Vivisector
31
+
32
+ class Anatomy < ImageLoader
33
+
34
+ def initialize
35
+ super
36
+ @order = [] # will hold the sorted indexes into the image set array
37
+ end
38
+
39
+ def allow_image_set image_set
40
+ raise ImplementationError, "Got a nil DesignImageSet object; was #{self.class.name} properly extended?" if image_set.nil?
41
+
42
+ # Ensure that we are only looking at design images
43
+ raise IncompatibilityError, "Tried to add a non- DesignImageSet object to Anatomy" unless image_set.is_a? DesignImageSet
44
+ end
45
+
46
+ end
47
+
48
+
49
+ class Appography < ImageLoader
50
+
51
+ def allow_image_set image_set
52
+ raise ImplementationError, "Got a nil DesignImageSet object; was #{self.class.name} properly extended?" if image_set.nil?
53
+
54
+ # Ensure that we are only looking at implementation images
55
+ raise IncompatibiltyError, "Tried to add a DesignImageSet object to Appography" if image_set.is_a? DesignImageSet
56
+ end
57
+
58
+ end
59
+
60
+ end
@@ -0,0 +1,119 @@
1
+
2
+ module Vivisector
3
+
4
+ # Container class for sets of images
5
+ class ImageSet
6
+
7
+ attr_reader :images # the container for all the images in the set
8
+
9
+ def initialize
10
+ @images = []
11
+ @cache_valid = false # cache invalidation is so easy
12
+ end
13
+
14
+ # Safe way to add images to the container
15
+ def add image
16
+ self.allow image
17
+ # fix relative paths
18
+ image.path = File.expand_path(image.path)
19
+ @images << image
20
+ @cache_valid = false
21
+ nil
22
+ end
23
+
24
+ # Raise an error if the image is not appropriate for this type of set
25
+ def allow image
26
+ puts " +++ If you're seeing this, #{self.class.name}.#{__method__} was not overridden"
27
+ end
28
+ end
29
+
30
+
31
+ # Design image sets need to be able to report on the images they contain
32
+ class DesignImageSet < ImageSet
33
+
34
+ def allow image
35
+ # Ensure that image objects have an "attribute" field, among other things
36
+ raise IncompatibilityError, "Tried to add a non- DesignImage object to a DesignImageSet" unless image.is_a? DesignImage
37
+
38
+ # no duplicates allowed
39
+ if (@images.map { |i| [i.id, i.attribute] }).include? [image.id, image.attribute]
40
+ raise OperationalError, "Tried to add an image with duplicate ID and attribute"
41
+ end
42
+ end
43
+
44
+ # Get the list of unique attributes contained by the images within
45
+ def contained_attributes
46
+ (@images.map { |i| i.attribute}).uniq
47
+ end
48
+
49
+ # cache the image attributes
50
+ def cache_image_attributes
51
+ return if @cache_valid
52
+
53
+ # make a hash -- hash[image id] = list of attributes defined for this image id
54
+ # we use this for convenience later
55
+ @attributes_by_id = {}
56
+ @images.each do |img|
57
+ proper_key = img.id.to_s
58
+ @attributes_by_id[proper_key] = [] unless @attributes_by_id.has_key? proper_key
59
+ @attributes_by_id[proper_key] << img.attribute
60
+ end
61
+
62
+ @cache_valid = true
63
+ end
64
+
65
+ # get the list of images that are valid for a particular attribute
66
+ def images_for_attribute attribute
67
+ self.cache_image_attributes
68
+
69
+ # return the pared-down list
70
+ @images.select do |img|
71
+ # this covers nil == nil and attribute == attribute
72
+ next true if img.attribute == attribute
73
+
74
+ # if there is no attribute for this image, it should be pulled in
75
+ # ... unless there's an exact match elsewhere in the set.
76
+ next true if img.attribute.nil? unless @attributes_by_id[img.id.to_s].include? attribute
77
+
78
+ false
79
+ end
80
+ end
81
+
82
+ end
83
+
84
+
85
+ # App image sets are tied to a target
86
+ class AppImageSet < ImageSet
87
+
88
+ attr_accessor :target
89
+
90
+ def allow image
91
+ # no duplicates
92
+ if (@images.map { |i| i.id}).include? image.id
93
+ raise OperationalError, "Tried to add an image with duplicate ID"
94
+ end
95
+
96
+ # App image sets don't need to worry about specific fields, but we keep it clean and symmetric.
97
+ raise IncompatibilityError, "Tried to add a DesignImage object to a non- DesignImageSet" if image.is_a? DesignImage
98
+ end
99
+
100
+ def cache_image_lookups
101
+ return if @cache_valid
102
+
103
+ @image_lookup = {}
104
+ @images.each do |img|
105
+ @image_lookup[img.id.to_s] = img
106
+ end
107
+
108
+ @cache_valid = true
109
+ end
110
+
111
+ def best_image_for(id)
112
+ self.cache_image_lookups
113
+ @image_lookup.fetch(id.to_s, nil)
114
+ end
115
+
116
+ end
117
+
118
+
119
+ end
@@ -0,0 +1,110 @@
1
+ require 'markaby'
2
+ require_relative 'filesystem'
3
+
4
+ module Vivisector
5
+
6
+ class Report
7
+ attr_accessor :title
8
+ attr_accessor :attribute
9
+ attr_accessor :anatomy
10
+ attr_accessor :appographies
11
+ attr_accessor :web_document_root
12
+ attr_accessor :destination_path
13
+ attr_accessor :img_width_px
14
+
15
+ def css
16
+ width_string = ""
17
+ width_string = "width: #{@img_width_px}px;" unless img_width_px.nil?
18
+ %(
19
+ body {color:white; background-color:#333;}
20
+ table.comparison th {border-top: 1px solid #ccc;}
21
+ table.comparison td {padding-bottom:1ex; text-align:center;}
22
+ .masterimg, .appimg {background-color:white; #{width_string}}
23
+ .missing {white-space: pre; text-align:center;}
24
+ )
25
+ end
26
+
27
+ # if necessary, modify the path to be relative (for web-based reports)
28
+ def path_transform element_path
29
+ unless @web_document_root.nil?
30
+ element_path = web_relative_path(@web_document_root, @destination_path, element_path)
31
+ end
32
+ element_path
33
+ end
34
+
35
+ def to_s
36
+ # initial calculations
37
+ candidates = @appographies.select do |appog|
38
+ next true if @attribute.nil? # unless there is nothing in this candidate???? might be expensive to check.
39
+
40
+ appog.image_set.target.attribute == @attribute
41
+ end
42
+ design_images = @anatomy.image_set.images_for_attribute(@attribute)
43
+ columns = candidates.length + 1
44
+
45
+ me = self
46
+
47
+ # build html
48
+ mab = Markaby::Builder.new
49
+ mab.html do
50
+
51
+ head do
52
+ title "#{me.title} | Vivisector"
53
+ style :type => "text/css" do
54
+ me.css
55
+ end
56
+ end
57
+
58
+ body do
59
+ h1 me.title
60
+ if design_images.empty?
61
+ p "No input images were found."
62
+ else
63
+ table.comparison do
64
+ # print out the first row of the table -- the target names
65
+ tr do
66
+ td "Design"
67
+ candidates.each do |c|
68
+ td c.image_set.target.name
69
+ end
70
+ end
71
+
72
+ # print out all the compared images
73
+ design_images.each do |design_image|
74
+ # title
75
+ tr do
76
+ th :colspan => columns do
77
+ a :name => design_image.description do
78
+ design_image.description
79
+ end
80
+ end
81
+ end
82
+
83
+ # images
84
+ tr do
85
+ td :align => "right", :valign => "top" do
86
+ img.masterimg :src => me.path_transform(design_image.path), :alt => design_image.description
87
+ end
88
+
89
+ candidates.each do |candidate|
90
+ app_image = candidate.image_set.best_image_for(design_image.id)
91
+ if app_image.nil?
92
+ td { div.missing "#{design_image.description} on #{candidate.image_set.target.name}" }
93
+ else
94
+ td :align => "left", :valign => "top" do
95
+ div.holder { img.appimg :src => me.path_transform(app_image.path), :alt => app_image.description }
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ return mab.to_s
106
+ end
107
+
108
+ end #Report
109
+
110
+ end
@@ -0,0 +1,45 @@
1
+ require_relative 'filesystem'
2
+
3
+ module Vivisector
4
+
5
+ class ReportSet
6
+ attr_accessor :anatomy
7
+ attr_accessor :appographies
8
+ attr_accessor :title_for_attribute_fn
9
+ attr_accessor :path_for_attribute_fn
10
+ attr_accessor :web_document_root
11
+ attr_accessor :img_width_px
12
+
13
+ # Check whether the path is correct, particularly if we are making a web-based report
14
+ def allow_path path
15
+ unless @web_document_root.nil?
16
+ unless ancestor?(@web_document_root, path)
17
+ raise OperationalError, "Report #{path} is not an ancestor of the web root #{@web_document_root}"
18
+ end
19
+ end
20
+ end
21
+
22
+ def write
23
+ @anatomy.image_set.contained_attributes.map do |attr|
24
+
25
+ # first check whether the destination is ok
26
+ path = @path_for_attribute_fn.call(attr)
27
+ self.allow_path path
28
+
29
+ r = Report.new
30
+ r.title = @title_for_attribute_fn.call(attr)
31
+ r.attribute = attr
32
+ r.anatomy = @anatomy
33
+ r.appographies = @appographies
34
+ r.web_document_root = @web_document_root
35
+ r.destination_path = path
36
+ r.img_width_px = @img_width_px
37
+
38
+ File.open(path, 'w') {|f| f.write(r.to_s) }
39
+ end
40
+ end
41
+
42
+ end
43
+
44
+
45
+ end #Vivisector
@@ -0,0 +1,9 @@
1
+
2
+ module Vivisector
3
+
4
+ class Target
5
+ attr_accessor :name # the friendly name of this target
6
+ attr_accessor :attribute # the attribute that this target can have
7
+ end
8
+
9
+ end
@@ -0,0 +1,3 @@
1
+ module Vivisector
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,40 @@
1
+ require 'vivisector/version'
2
+
3
+ require 'vivisector/exceptions'
4
+ require 'vivisector/target'
5
+ require 'vivisector/image'
6
+ require 'vivisector/imageset'
7
+ require 'vivisector/imageloader'
8
+ require 'vivisector/report'
9
+ require 'vivisector/reportset'
10
+
11
+
12
+ module Vivisector
13
+
14
+ def self.report(anatomy, appographies, title_for_attribute_fn, path_for_attribute_fn, web_document_root, img_width_px)
15
+ # load source images
16
+ anatomy.load
17
+ appographies.each { |appog| appog.load }
18
+
19
+ unless web_document_root.nil?
20
+ appographies.each do |appog|
21
+ # if the path isn't within web_document_root, throw
22
+ end
23
+
24
+ # copy all images to an images/ directory inside web_document_root
25
+ # mangle all image sets to point to there
26
+ # use dirnames like "images/design", "images/target1", "images/target2", etc
27
+ end
28
+
29
+ rs = Vivisector::ReportSet.new
30
+ rs.anatomy = anatomy
31
+ rs.appographies = appographies
32
+ rs.title_for_attribute_fn = title_for_attribute_fn
33
+ rs.path_for_attribute_fn = path_for_attribute_fn
34
+ rs.web_document_root = web_document_root
35
+ rs.img_width_px = img_width_px
36
+
37
+ rs.write
38
+ end
39
+
40
+ end
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vivisector
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ian Katz
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2015-05-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ - - '>='
21
+ - !ruby/object:Gem::Version
22
+ version: 1.3.6
23
+ type: :development
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.3'
30
+ - - '>='
31
+ - !ruby/object:Gem::Version
32
+ version: 1.3.6
33
+ - !ruby/object:Gem::Dependency
34
+ name: rake
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ~>
38
+ - !ruby/object:Gem::Version
39
+ version: '10.0'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ~>
45
+ - !ruby/object:Gem::Version
46
+ version: '10.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: markaby
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '0.8'
54
+ - - '>='
55
+ - !ruby/object:Gem::Version
56
+ version: 0.8.0
57
+ type: :runtime
58
+ prerelease: false
59
+ version_requirements: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ~>
62
+ - !ruby/object:Gem::Version
63
+ version: '0.8'
64
+ - - '>='
65
+ - !ruby/object:Gem::Version
66
+ version: 0.8.0
67
+ description: Vivisector helps you see inside your apps, specifically so that designers
68
+ can be part of a QA / Continuous Integration process. It's a framework to help you
69
+ compare design 'master' images to actual screenshots from various implementations.
70
+ email:
71
+ - ifreecarve@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - gem/lib/vivisector.rb
77
+ - gem/lib/vivisector/exceptions.rb
78
+ - gem/lib/vivisector/filesystem.rb
79
+ - gem/lib/vivisector/image.rb
80
+ - gem/lib/vivisector/imageloader.rb
81
+ - gem/lib/vivisector/imageset.rb
82
+ - gem/lib/vivisector/report.rb
83
+ - gem/lib/vivisector/reportset.rb
84
+ - gem/lib/vivisector/target.rb
85
+ - gem/lib/vivisector/version.rb
86
+ homepage: http://github.com/ifreecarve/vivisector
87
+ licenses:
88
+ - Apache 2.0
89
+ metadata: {}
90
+ post_install_message:
91
+ rdoc_options: []
92
+ require_paths:
93
+ - gem/lib
94
+ required_ruby_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ requirements: []
105
+ rubyforge_project:
106
+ rubygems_version: 2.4.6
107
+ signing_key:
108
+ specification_version: 4
109
+ summary: Design QA tool
110
+ test_files: []