View Javadoc
1   /*
2    * #%L
3    * wcm.io
4    * %%
5    * Copyright (C) 2018 wcm.io
6    * %%
7    * Licensed under the Apache License, Version 2.0 (the "License");
8    * you may not use this file except in compliance with the License.
9    * You may obtain a copy of the License at
10   *
11   *      http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   * #L%
19   */
20  package io.wcm.qa.glnm.galen.specs.imagecomparison;
21  
22  import static io.wcm.qa.glnm.configuration.GaleniumConfiguration.getActualImagesDirectory;
23  import static io.wcm.qa.glnm.configuration.GaleniumConfiguration.getExpectedImagesDirectory;
24  import static io.wcm.qa.glnm.util.FileHandlingUtil.constructRelativePath;
25  
26  import java.awt.image.BufferedImage;
27  import java.io.File;
28  import java.io.IOException;
29  import java.util.Collection;
30  import java.util.HashSet;
31  import java.util.List;
32  import java.util.regex.Matcher;
33  import java.util.regex.Pattern;
34  
35  import javax.imageio.ImageIO;
36  
37  import org.apache.commons.io.FileUtils;
38  import org.apache.commons.io.FilenameUtils;
39  import org.apache.commons.lang3.StringUtils;
40  import org.slf4j.Logger;
41  import org.slf4j.LoggerFactory;
42  
43  import com.galenframework.parser.SyntaxException;
44  import com.galenframework.speclang2.specs.SpecReader;
45  import com.galenframework.specs.Spec;
46  import com.galenframework.validation.ImageComparison;
47  import com.galenframework.validation.ValidationError;
48  import com.galenframework.validation.ValidationResult;
49  
50  import io.wcm.qa.glnm.exceptions.GaleniumException;
51  import io.wcm.qa.glnm.selectors.base.Selector;
52  import io.wcm.qa.glnm.util.FileHandlingUtil;
53  
54  /**
55   * Utility methods for Galenium image comparison via Galen.
56   */
57  final class IcUtil {
58  
59    private static final BufferedImage DUMMY_IMAGE = new BufferedImage(20, 20, BufferedImage.TYPE_3BYTE_BGR);
60  
61    private static final String DUMMY_IMAGE_FORMAT = "png";
62  
63    private static final Logger LOG = LoggerFactory.getLogger(IcUtil.class);
64  
65    private static final String REGEX_IMAGE_FILENAME = ".*image file ([^,]*\\.png).*";
66    static final Pattern REGEX_PATTERN_IMAGE_FILENAME = Pattern.compile(REGEX_IMAGE_FILENAME);
67  
68    private IcUtil() {
69      // do not instantiate
70    }
71  
72    private static String getImageComparisonSpecText(String folder, String fileName, String error, int offset, List<Selector> toIgnore) {
73      StringBuilder specText = new StringBuilder()
74      // boiler plate
75          .append("image file ")
76      // image file
77          .append(getImageOrDummySamplePath(folder, fileName));
78  
79      // tolerance
80      if (StringUtils.isNotBlank(error)) {
81        specText.append(", error ")
82            .append(error);
83      }
84      if (offset > 0) {
85        specText.append(", analyze-offset ");
86        specText.append(offset);
87      }
88      if (!toIgnore.isEmpty()) {
89        List<Selector> objects = toIgnore;
90        specText.append(", ignore-objects ");
91        if (objects.size() == 1) {
92          specText.append(objects.get(0));
93        }
94        else {
95          specText.append("[");
96          Collection<String> elementNames = new HashSet<String>();
97  
98          for (Selector object : objects) {
99            elementNames.add(object.elementName());
100         }
101 
102         specText.append(StringUtils.join(elementNames, ", "));
103         specText.append("]");
104       }
105     }
106 
107     return specText.toString();
108   }
109 
110   private static String getImageOrDummySamplePath(String folder, String fileName) {
111     String fullFilePath;
112 
113     // folder
114     if (StringUtils.isNotBlank(folder)) {
115       fullFilePath = FilenameUtils.concat(folder, fileName);
116     }
117     else {
118       // no folder means fileName is all the path info we have
119       fullFilePath = fileName;
120     }
121 
122     createDummyIfSampleDoesNotExist(fullFilePath);
123 
124     return fullFilePath;
125   }
126 
127   private static File getOriginalFilteredImage(ValidationResult result) {
128     ImageComparison imageComparison = getImageComparison(result);
129     if (imageComparison == null) {
130       return null;
131     }
132     File actualImage = imageComparison.getOriginalFilteredImage();
133     if (actualImage == null) {
134       LOG.debug("could not find sampled image in image comparison.");
135     }
136 
137     return actualImage;
138   }
139 
140   private static File getSampleTargetFile(Spec spec) {
141     String targetPath = getTargetPathFrom(spec);
142     File imageFile = new File(targetPath);
143     FileHandlingUtil.ensureParent(imageFile);
144     return imageFile;
145   }
146 
147   private static String getTargetPathFrom(Spec spec) {
148     File rootDirectory = new File(getExpectedImagesDirectory());
149     String imagePathFromSpec = getImagePathFrom(spec);
150     String relativeImagePath = constructRelativePath(rootDirectory, new File(imagePathFromSpec));
151     return getActualImagesDirectory() + File.separator + relativeImagePath;
152   }
153 
154   private static boolean isExpectedImageSampleMissing(String fullFilePath) {
155     return !new File(fullFilePath).isFile();
156   }
157 
158   private static File writeDummySample(File targetFile) {
159     try {
160       if (LOG.isTraceEnabled()) {
161         LOG.trace("begin writing dummy image '" + targetFile);
162       }
163       FileHandlingUtil.ensureParent(targetFile);
164       if (ImageIO.write(DUMMY_IMAGE, DUMMY_IMAGE_FORMAT, targetFile)) {
165         if (LOG.isDebugEnabled()) {
166           LOG.debug("done writing dummy image '" + targetFile);
167         }
168       }
169       else if (LOG.isInfoEnabled()) {
170         LOG.info("could not write dummy image '" + targetFile);
171       }
172       return targetFile;
173     }
174     catch (IOException ex) {
175       throw new GaleniumException("could not write dummy image.", ex);
176     }
177   }
178 
179   static void createDummyIfSampleDoesNotExist(String fullFilePath) {
180     if (IcUtil.isExpectedImageSampleMissing(fullFilePath)) {
181       if (LOG.isInfoEnabled()) {
182         LOG.info("Cannot find sample. Substituting dummy for '" + fullFilePath + "'");
183       }
184 
185       // if image is missing, we'll substitute a dummy to force Galen to at least sample the page
186       File targetFile = new File(fullFilePath);
187 
188       writeDummySample(targetFile);
189     }
190   }
191 
192   static ImageComparison getImageComparison(ValidationResult result) {
193     ValidationError error = result.getError();
194     if (error == null) {
195       LOG.debug("could not find error in validation result.");
196       return null;
197     }
198 
199     ImageComparison imageComparison = error.getImageComparison();
200     if (imageComparison == null) {
201       LOG.debug("could not find image comparison in validation error.");
202       return null;
203     }
204 
205     return imageComparison;
206   }
207 
208   static String getImageComparisonSpecText(IcsDefinition def) {
209     return IcUtil.getImageComparisonSpecText(
210         def.getFoldername(),
211         def.getFilename(),
212         def.getAllowedError(),
213         def.getAllowedOffset(),
214         def.getObjectsToIgnore());
215   }
216 
217   static String getImagePathFrom(Spec spec) {
218     Matcher matcher = REGEX_PATTERN_IMAGE_FILENAME.matcher(spec.toText());
219     if (matcher.matches() && matcher.groupCount() >= 1) {
220       return matcher.group(1);
221     }
222     return "";
223   }
224 
225   static File getSampleSourceFile(Spec spec, ValidationResult result) {
226     File imageFile = getOriginalFilteredImage(result);
227     if (imageFile != null) {
228       if (LOG.isDebugEnabled()) {
229         LOG.debug("sample source file: " + imageFile.getPath());
230       }
231       return imageFile;
232     }
233     String imagePath = getImagePathFrom(spec);
234     if (StringUtils.isBlank(imagePath)) {
235       if (LOG.isWarnEnabled()) {
236         LOG.warn("could not extract image name from: " + spec.toText());
237       }
238       return null;
239     }
240     if (LOG.isDebugEnabled()) {
241       LOG.debug("sample source path: " + imagePath);
242     }
243     return new File(imagePath);
244   }
245 
246   static Spec getSpecForText(String specText) {
247     try {
248       return new SpecReader().read(specText);
249     }
250     catch (IllegalArgumentException | SyntaxException ex) {
251       String msg = "when parsing spec text: '" + specText + "'";
252       LOG.error(msg);
253       throw new GaleniumException(msg, ex);
254     }
255   }
256 
257   static String getZeroToleranceImageComparisonSpecText(IcsDefinition def) {
258     return getImageComparisonSpecText(
259         def.getFoldername(),
260         def.getFilename(),
261         "",
262         0,
263         def.getObjectsToIgnore());
264   }
265 
266   static boolean isImageComparisonSpec(Spec spec) {
267     return StringUtils.contains(spec.toText(), "image file ");
268   }
269 
270   static void saveSample(String objectName, Spec spec, ValidationResult result) {
271     if (LOG.isDebugEnabled()) {
272       LOG.debug("checking for image file: " + spec.toText() + " (with regex: " + REGEX_PATTERN_IMAGE_FILENAME.pattern() + ")");
273     }
274     File source = getSampleSourceFile(spec, result);
275     if (source == null) {
276       if (LOG.isDebugEnabled()) {
277         LOG.debug("did not find source file: " + objectName);
278       }
279       return;
280     }
281 
282     File target = getSampleTargetFile(spec);
283     if (LOG.isTraceEnabled()) {
284       LOG.trace("begin copying image '" + source + "' -> '" + target + "'");
285     }
286     try {
287       FileUtils.copyFile(source, target);
288     }
289     catch (GaleniumException | IOException ex) {
290       String msg = "could not write image: " + target;
291       LOG.error(msg, ex);
292     }
293     if (LOG.isTraceEnabled()) {
294       LOG.trace("done copying image '" + source + "' -> '" + target + "'");
295     }
296   }
297 
298 }