Added project

This commit is contained in:
Александр Александров
2023-06-09 00:43:36 +03:00
parent 113901abf0
commit 33a74f6bfe
17 changed files with 7234 additions and 65 deletions

66
.gitignore vendored
View File

@@ -1,52 +1,14 @@
# ---> Kotlin
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*
# ---> Java
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
replay_pid*
/target/
/.idea/
/*.iml
/chromedriver
/geckodriver
/operadriver
/*.svg
/*.png
/*.properties
/svgs/
/out/
/*.html
/*.exe
/*.log

20
LICENSE
View File

@@ -1,9 +1,21 @@
MIT License
Copyright (c) <year> <copyright holders>
Copyright (c) 2018 Alexandr
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,10 +1,52 @@
# visual-quality-control-tool
[![pipeline status](https://gitlab.resprojects.ru/mrresident/visual-quality-control-tool/badges/master/pipeline.svg)](https://gitlab.resprojects.ru/mrresident/visual-quality-control-tool/commits/master)
VQCT (Visual quality control tool)
# VQCT (Visual quality control tool)
Основные возможности:
## Основные возможности:
1. Рендер поданного на вход svg файла в браузере.
2. Создание скриншот получившегося рендера в браузере, с последующим сохранением в формате png в выбранный каталог.
3. Сравнительный анализ получившегося результата с оценочным png файлом, который был также подан на вход программе.
4. Формирование результата сравнения.
Программа работает через комнадную строку, ниже перечислены параметры запуска:
Usage: `[keys] <path_to_svg_file> <path_to_base_png_file>`
Overview:
[keys]
`--help` - вывод подсказки с кратким описанием параметров запуска программы.
`--setup` - запускает мастер установки, где пользователю предлагается указать пути к браузерам (chrome, opera, firefox и ie), веб-драйверам для браузеров, путь где установлен ImageMagick, а так же два параметра которые используется утилитой compare в ImageMagick. Может использоваться с другими ключами.
`--load-settings=<path_to_file_settings>` - задать путь к файлу настроек программы `<settings_file_name>.properties`. Если по указанному пути файл не найден, то будет автоматически создан файл с настройками пол умолчанию. Если запустить программу с ключём `--setup`, то вызовиться мастер установки, данные которого будут сохранены в указанным ключем `--load-settings` файл.
`--browsers=<list_browsers>` - список браузеров которые будут использованы во время работы программы. Данные перечисляются через запятую. Допустимые значения для данного ключа: `chrome, opera, firefox и ie`. Если не задать данный ключ при запуске, то по умолчанию будут задействованы все четыре браузера. `Example: --browsers=opera,ie`
`--out=<path_to_output_screenshot_dir>` - полный путь к выходному каталогу для скриншотов. Если каталог не существует, автоматически создается каталог и подкаталоги. Если путь не указан, скриншоты будут храниться в `{current_execute_dir}/{file_name_with_ext} _screenshots`
`<path_to_svg_file>` - путь к SVG файлу. `Example: /home/user/pics/svg_file.svg`. Если полные путь не указан, тогда будет использоваться следующее `{current_execute_dir}/svg_file.svg`.
`<path_to_base_png_file>` - путь к базовому (контрольному) PNG файлу. `Example: /home/user/pics/base_png_file.png`. Если полные путь не указан, тогда будет использоваться следующее `{current_execute_dir}/base_png_file.png`.
Для корректной работы необходимо следующее:
1. Браузеры - Chrome/Chromium, Opera, Firefox и IE (если программа запускается в windows)
2. Selenium webdriver для выше перечисленных браузеров.
3. ImageMagick v 6.x.x (для работы с ImageMagick используется java wrapper - im4java)
## Рендер svg файла в браузере
Процесс работы:
1. Инициализация веб-драйвера посредствам перечисления параметров запуска выбранного браузера.
2. Задание размеров окна браузера - ширина и высота;
3. Задание ссылки на ресурс, который необходимо открыть в веб-браузере;
4. Непосредственно рендер открытого ресурса;
5. Сохранение результата рендера в файл формата png по указанному, при запуске программы, пути.
## Сравнение png файлов
Данный этап представляет собой сравнительный анализ выходных данных, полученных при рендере исходного svg файла в выбранных браузерах, с оценочным png файлом. Программа должна сообщить, идентичны ли сравниваемые png файлы с оценочным или нет. Если есть отличия, то будет сформирован графический файл в формате png, в котором будут отображаться области в которых нашлись отличия. Для сравнения использует поставляемая в комплект ImageMagick утилита - compare (более подробно, можно прочитать тут https://www.imagemagick.org/Usage/compare/#metrics).

116
pom.xml Normal file
View File

@@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ru.resprojects</groupId>
<artifactId>vqct</artifactId>
<version>0.1</version>
<packaging>jar</packaging>
<name>vqct</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<kotlin.version>1.2.30</kotlin.version>
<main.class>ru.resprojects.MainKt</main.class>
</properties>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test-junit</artifactId>
<version>${kotlin.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>3.8.1</version>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-api</artifactId>
<version>3.8.1</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>org.im4java</groupId>
<artifactId>im4java</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>io.github.microutils</groupId>
<artifactId>kotlin-logging</artifactId>
<version>1.5.3</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.5</version>
</dependency>
</dependencies>
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.6</version>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<archive>
<manifest>
<mainClass>${main.class}</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,206 @@
/*
MIT License
Copyright (c) 2018 Aleksandr
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
package ru.resprojects
import ru.resprojects.vqct.*
import mu.KotlinLogging
import java.io.File
/**
* @author Aleksandr Aleksandrov aka mrResident
*/
private val logger = KotlinLogging.logger {}
val usageMessage = """
Argument list error!
Usage: [keys] <path_to_svg_file> <path_to_base_png_file>
Overview:
[keys]
--setup - running setup wizard.
--load-settings=<path_to_file_settings> - start the program with an alternative path to the settings file.
--browsers=<list_browsers> - run the program with a list of browsers that should be launched to load svg and taking screenshot.
Browsers list are separated by commas. Valid values for this key: chrome, firefox, opera, ie. Example: --browsers=opera,ie
--out=<path_to_output_screenshot_dir> - full path to output directory for screenshots.
If directory is not existing then directory and subdirectories automatically is created.
If path is not specified then screenshots will be storage in {current_execute_dir}/{file_name_with_ext}_screenshots
<path_to_svg_file> - path to SVG file. Example: /home/user/pics/svg_file.svg.
If full path is not specified then used {current_execute_dir}/svg_file.svg.
<path_to_base_png_file> - path to base (control) PNG file. Example: /home/user/pics/base_png_file.png.
If full path is not specified then used {current_execute_dir}/base_png_file.png.
""".trimIndent()
/**
* Entry point for program.
* @param args arguments
*/
fun main(args: Array<String>) {
println("Starting work...")
try {
VQCT(parseArguments(args)).launch()
} catch (err: IllegalArgumentException) {
println("${err.message}")
println("Working stopped with ERROR!")
} catch (err: Exception) {
logger.error(err) { "$err" }
println("Working stopped with ERROR! For more information see logger.log file!")
}
}
/**
* Input argument list parse.
* @param args input program argument list
* @return ArgumentsData - parsed input data
*/
fun parseArguments(args: Array<String>): ArgumentsData {
if (args.isEmpty()) {
logger.error { "Arguments count error. Must be specified minimum two arguments <path_to_svg_file> <path_to_png_file>" }
throw IllegalArgumentException(usageMessage)
}
val argumentsData = ArgumentsData()
args.forEach {
if (it.toLowerCase() == "--help") {
throw IllegalArgumentException(usageMessage)
}
if (it.toLowerCase() == "--setup") {
argumentsData.isSetup = true
}
if (it.endsWith(".svg", true)) {
argumentsData.svgFile = File(it)
if (!argumentsData.svgFile!!.exists()) {
logger.error { "ERROR: File ${argumentsData.svgFile!!.absolutePath} is not found!" }
throw IllegalArgumentException("ERROR: File ${argumentsData.svgFile!!.absolutePath} is not found!")
}
}
if (it.endsWith(".png", true)) {
argumentsData.basePngFile = File(it)
if (!argumentsData.basePngFile!!.exists()) {
logger.error { "ERROR: File ${argumentsData.basePngFile!!.absolutePath} is not found!" }
throw IllegalArgumentException("ERROR: File ${argumentsData.basePngFile!!.absolutePath} is not found!")
}
}
}
if ((argumentsData.svgFile == null || argumentsData.basePngFile == null) && !argumentsData.isSetup) {
logger.error { "ERROR: Not specified SVG file or Base PNG file! Must be specified minimum two arguments <path_to_svg_file> <path_to_png_file>" }
throw IllegalArgumentException("ERROR: Not specified SVG file or Base PNG file! Must be specified minimum two arguments <path_to_svg_file> <path_to_png_file>")
} else {
if ((argumentsData.svgFile == null && argumentsData.basePngFile == null) && argumentsData.isSetup) {
return argumentsData
} else if ((argumentsData.svgFile == null || argumentsData.basePngFile == null) && argumentsData.isSetup) {
logger.error { "ERROR: Not specified SVG file or Base PNG file! Must be specified minimum two arguments <path_to_svg_file> <path_to_png_file>" }
throw IllegalArgumentException("ERROR: Not specified SVG file or Base PNG file! Must be specified minimum two arguments <path_to_svg_file> <path_to_png_file>")
}
}
args.forEach {
if (!(it.endsWith(".png", true) || it.endsWith(".svg", true))) {
if (it.startsWith("--")) {
if (it.startsWith("--load-settings", true)) {
if (it.split("=").size == 2 && it.split("=")[1].isNotEmpty()) {
argumentsData.settingsFile = File(it.split("=")[1])
if (argumentsData.settingsFile!!.exists()) {
if (argumentsData.settingsFile!!.extension != "properties") {
logger.error { "Invalid properties file ${argumentsData.settingsFile!!.absolutePath}!" }
throw IllegalArgumentException("ERROR: Invalid properties file ${argumentsData.settingsFile!!.absolutePath}!")
}
} else {
logger.error { "Properties file ${argumentsData.settingsFile!!.absolutePath} is not found!" }
throw IllegalArgumentException("ERROR: Properties file ${argumentsData.settingsFile!!.absolutePath} is not found!")
}
} else {
logger.error { "Invalid key value --load-settings. Must be --load-settings=<path_to_properties_file>" }
throw IllegalArgumentException("ERROR: Invalid key value --load-settings. Must be --load-settings=<path_to_properties_file>")
}
}
if (it.startsWith("--browsers", true)) {
if (it.split("=").size == 2 && it.split("=")[1].isNotEmpty()) {
argumentsData.browsers = it.split("=")[1].toUpperCase().split(",")
for (i in 0 .. (argumentsData.browsers!!.size - 1)) {
try {
Browsers.valueOf(argumentsData.browsers!![i])
} catch (err: Exception) {
logger.error { "Invalid value \"${argumentsData.browsers!![i]}\" for key --browsers. Must be --browsers=<list_of_browsers>. Browsers list are separated by commas. Valid values for this key: chrome, firefox, opera, ie." }
throw IllegalArgumentException("ERROR: Invalid value \"${argumentsData.browsers!![i]}\" for key --browsers. Must be --browsers=<list_of_browsers>. Browsers list are separated by commas. Valid values for this key: chrome, firefox, opera, ie.")
}
}
} else {
logger.error { "Invalid key value --browsers. Must be --browsers=<list_of_browsers>. Browsers list are separated by commas. Valid values for this key: chrome, firefox, opera, ie." }
throw IllegalArgumentException("ERROR: Invalid key value --browsers. Must be --browsers=<list_of_browsers>. Browsers list are separated by commas. Valid values for this key: chrome, firefox, opera, ie.")
}
}
if (it.startsWith("--out", true)) {
if (it.toLowerCase().split("=").size == 2 && it.split("=")[1].isNotEmpty()) {
argumentsData.outputDirectory = if (it.split("=")[1].endsWith("/") || it.split("=")[1].endsWith("\\")) {
File("${it.split("=")[1]}${argumentsData.svgFile!!.name.replace('.', '_')}_screenshots")
} else {
File("${it.split("=")[1]}${System.getProperty("file.separator")}${argumentsData.svgFile!!.name.replace('.', '_')}_screenshots")
}
} else {
logger.error { "Invalid key value --out. Must be --out=<path_to_output_screenshot_dir>." }
throw IllegalArgumentException("ERROR: Invalid key value --out. Must be --out=<path_to_output_screenshot_dir>.")
}
}
} else {
logger.error { "Invalid input data \"$it\"" }
throw IllegalArgumentException("Invalid input data \"$it\".\n$usageMessage")
}
}
}
if (argumentsData.outputDirectory == null) {
argumentsData.outputDirectory = File("${System.getProperties().getProperty("user.dir")}${System.getProperty("file.separator")}${argumentsData.svgFile!!.name.replace('.', '_')}_screenshots")
}
if (argumentsData.browsers == null) {
val tmp = mutableListOf<String>()
Browsers.values().forEach {
tmp.add(it.name)
}
argumentsData.browsers = tmp.toList()
}
return argumentsData
}
/**
* Data storage for parse program argument list.
* @param svgFile - input svg file. This is file will be rendered in browser.
* @param basePngFile - input png file. This is base file for comparing with output screenshots.
* @param outputDirectory - full path to output directory for screenshots.
* @param settingsFile - settings file.
* @param browsers - list of browsers that should be launched to load svg and taking screenshot.
* @param isSetup - flag for setup wizard.
*/
data class ArgumentsData(
var svgFile: File? = null,
var basePngFile: File? = null,
var outputDirectory: File? = null,
var settingsFile: File? = null,
var isSetup: Boolean = false,
var browsers: List<String>? = null
)

View File

@@ -0,0 +1,272 @@
/*
MIT License
Copyright (c) 2018 Aleksandr
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
package ru.resprojects.vqct
import mu.KotlinLogging
import org.im4java.core.ImageCommand
import org.im4java.core.ConvertCmd
import org.im4java.core.IdentifyCmd
import org.im4java.core.CompositeCmd
import org.im4java.core.CompareCmd
import org.im4java.core.IMOperation
import org.im4java.process.ArrayListErrorConsumer
import org.im4java.process.ArrayListOutputConsumer
import org.openqa.selenium.Dimension
import java.awt.Toolkit
import java.io.File
import java.io.FileNotFoundException
/**
* @author Aleksandr Aleksandrov aka mrResident
*/
private val logger = KotlinLogging.logger {}
/**
* ImageMagick utilities
*/
private enum class CommandType {
CONVERT, IDENTIFY, COMPOSITECMD, COMPARE
}
/**
* ImgaeMagick tools.
* @param settings program settings
*/
class IMUtils(private val settings: ProgramSettings) {
/**
* Getting IM utilities.
* @param commandType ImageMagick utilities
* @return IM command
*/
private fun getImageCommand(commandType: CommandType): ImageCommand {
try {
val imPath = settings.getImageMagick().absolutePath
val imageCommand = when (commandType) {
CommandType.CONVERT -> ConvertCmd(false)
CommandType.IDENTIFY -> IdentifyCmd(false)
CommandType.COMPOSITECMD -> CompositeCmd(false)
CommandType.COMPARE -> CompareCmd(false)
}
imageCommand.searchPath = imPath
return imageCommand
} catch (err: FileNotFoundException) {
throw FileNotFoundException("${err.message}. ImageMagick v 6.x.x is not found! For download ImageMagick please visit https://legacy.imagemagick.org/script/download.php")
}
}
/**
* Getting info from graphic file.
* @param imageFile graphic file
* @return image information
*/
fun getImageInfo(imageFile: File): ImageInfo {
if (!imageFile.exists()) {
throw FileNotFoundException("Image file ${imageFile.absolutePath} not found!")
}
try {
val operation = IMOperation()
operation.format("%w,%h,%Q,%m")
operation.addImage()
val identyfyCmd = getImageCommand(CommandType.IDENTIFY)
val output = ArrayListOutputConsumer()
identyfyCmd.setOutputConsumer(output)
identyfyCmd.run(operation, imageFile.absolutePath)
val list = mutableListOf<String>()
list.addAll(output.output[0].split(","))
list.add(imageFile.absolutePath)
return ImageInfo(list)
} catch (err: Exception) {
throw IllegalStateException("Error while reading image file ${imageFile.absolutePath}. ${err.message}")
}
}
/**
* Cropping input image image. For more information see IM manual https://www.imagemagick.org/Usage/crop/#crop
* @param inputImage input image that needes cropping
* @param outputImage output cropping image
* @param dimension dimension (part or all image) for cropping
* @param x count of pixels for cropping
* @param y count of pixels for cropping
* @return false if appeared error while image cropping
*/
fun cropImage(inputImage: File, outputImage: File, dimension: Dimension, x: Int, y: Int): Boolean {
if (!inputImage.exists()) {
throw FileNotFoundException("Input file ${inputImage.absolutePath} not found!")
}
return try {
val operation = IMOperation()
operation.addImage(inputImage.absolutePath)
operation.crop(dimension.getWidth(), dimension.getHeight(), x, y)
operation.p_repage()
operation.addImage(outputImage.absolutePath)
val convert = getImageCommand(CommandType.CONVERT)
convert.run(operation)
true
} catch (err: Exception) {
logger.error(err) { "Error while cropping image: ${err.message}" }
false
}
}
/**
* Trimming input image image. For more information see IM manual https://www.imagemagick.org/Usage/crop/#trim
* @param inputImage input image that needes trimming
* @param outputImage output trimmed image
* @return false if appeared error while image trimming
*/
fun trimImage(inputImage: File, outputImage: File): Boolean {
if (!inputImage.exists()) {
throw FileNotFoundException("Input file ${inputImage.absolutePath} not found!")
}
return try {
val operation = IMOperation()
operation.addImage(inputImage.absolutePath)
operation.trim()
operation.p_repage()
operation.addImage(outputImage.absolutePath)
val convert = getImageCommand(CommandType.CONVERT)
convert.run(operation)
true
} catch (err: Exception) {
logger.error(err) { "Error while trimming image: ${err.message}" }
false
}
}
/**
* Resizing input image file. For more information see IM manual https://www.imagemagick.org/Usage/resize/#resize
* @param inputImage input image that needes resizing
* @param outputImage output resized image
* @param width new image width
* @param height new image height
* @return false if appeared error while image resized
*/
fun resizeImage(inputImage: File, outputImage: File, width: Int, height: Int): Boolean {
if (!inputImage.exists()) {
throw FileNotFoundException("Input file ${inputImage.absolutePath} not found!")
}
return try {
val operation = IMOperation()
operation.addImage(inputImage.absolutePath)
operation.resize(width, height)
operation.addImage(outputImage.absolutePath)
val convert = getImageCommand(CommandType.CONVERT)
convert.run(operation)
true
} catch (err: Exception) {
logger.error(err) { "Error while resizing image: ${err.message}" }
false
}
}
/**
* Comparing two image. For more information see IM manual https://www.imagemagick.org/Usage/compare/
* @param inputImage_1 first image for comparing
* @param inputImage_2 second image for comparing
* @param outputImage output image with comparing result
* @return absolute error count, number of different pixels
*/
fun compareImage(inputImage_1: File, inputImage_2: File, outputImage: File): Int {
if (!inputImage_1.exists()) {
throw FileNotFoundException("Input file ${inputImage_1.absolutePath} not found!")
}
if (!inputImage_2.exists()) {
throw FileNotFoundException("Input file ${inputImage_2.absolutePath} not found!")
}
val operation = IMOperation()
val compareCmd = getImageCommand(CommandType.COMPARE)
val error = ArrayListErrorConsumer()
compareCmd.setErrorConsumer(error)
operation.metric("ae")
operation.fuzz(settings.getImFuzz(), true)
operation.addImage(inputImage_1.absolutePath)
operation.addImage(inputImage_2.absolutePath)
operation.addImage(outputImage.absolutePath)
val isEquals = try {
compareCmd.run(operation)
true
} catch (err: Exception) {
false
}
return if (!isEquals) {
if (!error.output.isEmpty()) {
try {
error.output[0].toInt()
} catch (err: NumberFormatException) {
-1
}
} else {
-1
}
} else {
0
}
}
}
/**
* Image file information.
* @param inputData image information (returned from IM)
*/
data class ImageInfo(private val inputData: List<String>) {
/**
* Image size (width and height).
*/
val imageSize: Dimension
/**
* Image compression quality.
*/
val imageQuality: Double
/**
* Image file format.
*/
val imageFormat: String
/**
* Image file.
*/
val imageFile: File
init {
try {
imageSize = Dimension(inputData[0].toInt(), inputData[1].toInt())
imageQuality = inputData[2].toDouble()
imageFormat = inputData[3]
imageFile = File(inputData[4])
} catch (err: Exception) {
throw IllegalStateException("Can't get image info.")
}
}
}
/**
* Return current screen resolution.
*/
fun getCurrentScreenResolution(): Dimension {
return Dimension(Toolkit.getDefaultToolkit().screenSize.width, Toolkit.getDefaultToolkit().screenSize.height)
}

View File

@@ -0,0 +1,211 @@
/*
MIT License
Copyright (c) 2018 Aleksandr
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
package ru.resprojects.vqct
import org.apache.commons.io.FileUtils
import org.openqa.selenium.*
import org.openqa.selenium.chrome.ChromeDriver
import org.openqa.selenium.chrome.ChromeOptions
import org.openqa.selenium.firefox.FirefoxDriver
import org.openqa.selenium.firefox.FirefoxOptions
import org.openqa.selenium.ie.InternetExplorerDriver
import org.openqa.selenium.opera.OperaDriver
import org.openqa.selenium.opera.OperaOptions
import java.io.File
import java.io.FileNotFoundException
import java.io.FileWriter
import java.io.IOException
import java.util.concurrent.TimeUnit
import javax.imageio.ImageIO
/**
* @author Aleksandr Aleksandrov aka mrResident
*/
/**
* Render input svg file in browser and taking screenshot
*/
abstract class SvgRender(val settings: ProgramSettings) {
abstract fun getScreenshot(dimension: Dimension): File
/**
* HTML wrapper for SVG-file
* @param inputFile SVG-file
* @param dimension dimension for wrapped svg-file.
* @param patch place slash at the start for svg-file path. This is patch actual for windows-version of the Firefox browser.
* @return html file for SVG-file
*/
protected fun htmlWrapperCreate(inputFile: File, dimension: Dimension, patch: Boolean = false): File {
val outFile = File("${FileUtils.getTempDirectory()}${System.getProperty("file.separator")}wrapper.html")
val outHTML = FileWriter(outFile)
val htmlDoc = """
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Getting screenshot from svg file ${inputFile.name}</title>
</head>
<body>
<img name="svg_file" src="${if (patch) "/" else ""}${inputFile.absolutePath}" width="${dimension.getWidth()}" height="${dimension.getHeight()}" alt="svg_file">
</body>
</html>
""".trimIndent()
try {
outHTML.write(htmlDoc)
return outFile
} catch (err: IOException) {
throw IllegalStateException("Can not write to file ${outFile.absolutePath}")
} finally {
try {
outHTML.close()
} catch (err: IOException) {
throw IllegalStateException("Can not write to file ${outFile.absolutePath}")
}
}
}
/**
* Taking screenshot from browser.
* @param driver selenium WebDriver for browser.
* @return screenshot file
*/
protected fun shoot(driver: WebDriver): File {
return try {
val screen = (driver as TakesScreenshot).getScreenshotAs(OutputType.FILE)
val webElement = driver.findElement(By.name("svg_file"))
val img = ImageIO.read(screen)
val dest = img.getSubimage(webElement.location.getX(), webElement.location.getY(), webElement.size.getWidth(), webElement.size.getHeight())
ImageIO.write(dest, "png", screen)
driver.manage().window().size = Dimension(640, 480)
driver.quit()
screen
} catch (err: Exception) {
driver.quit()
throw IllegalStateException("Can not get correct screenshot! Cause: ${err.message}")
}
}
}
/**
* Opening svg file and taking screenshot in chrome/chromium browser.
*/
class SvgRenderByChrome(settings: ProgramSettings, private val svgFile: File): SvgRender(settings) {
override fun getScreenshot(dimension: Dimension): File {
try {
System.setProperty("webdriver.chrome.driver", settings.getWebDriver(Browsers.CHROME).absolutePath)
} catch (err: FileNotFoundException) {
throw IllegalStateException("Web driver for Google Chrome / Chromium is not found. Webdriver may be downloaded from https://sites.google.com/a/chromium.org/chromedriver/downloads")
}
val options = ChromeOptions()
try {
options.setBinary(settings.getBrowser(Browsers.CHROME).absolutePath)
} catch (err: FileNotFoundException) {
throw IllegalStateException("Browser Google Chrome / Chromium is not found")
}
val driver = ChromeDriver(options)
driver.manage().window().fullscreen()
driver.get(htmlWrapperCreate(svgFile, dimension).toURI().toString())
return shoot(driver)
}
}
/**
* Opening svg file and taking screenshot in Firefox browser.
*/
class SvgRenderByFirefox(settings: ProgramSettings, private val svgFile: File): SvgRender(settings) {
override fun getScreenshot(dimension: Dimension): File {
try {
System.setProperty("webdriver.firefox.driver", settings.getWebDriver(Browsers.FIREFOX).absolutePath)
} catch (err: FileNotFoundException) {
throw IllegalStateException("Web driver for Firefox is not found. Web driver can download from https://github.com/mozilla/geckodriver/releases")
}
val options = FirefoxOptions()
try {
options.setBinary(settings.getBrowser(Browsers.FIREFOX).absolutePath)
} catch (err: FileNotFoundException) {
throw IllegalStateException("Browser Firefox is not found")
}
val driver = FirefoxDriver(options)
driver.manage().window().fullscreen()
if (getOSName() == TypeOfOS.WINDOWS) {
driver.get(htmlWrapperCreate(svgFile, dimension, patch = true).toURI().toString())
} else {
driver.get(htmlWrapperCreate(svgFile, dimension).toURI().toString())
}
return shoot(driver)
}
}
/**
* Opening svg file and taking screenshot in Opera browser.
*/
class SvgRenderByOpera(settings: ProgramSettings, private val svgFile: File): SvgRender(settings) {
override fun getScreenshot(dimension: Dimension): File {
try {
System.setProperty("webdriver.opera.driver", settings.getWebDriver(Browsers.OPERA).absolutePath)
} catch (err: FileNotFoundException) {
throw IllegalStateException("Web driver for Opera is not found. Webdriver may be downloaded from https://github.com/operasoftware/operachromiumdriver/releases")
}
val options = OperaOptions()
try {
options.setBinary(settings.getBrowser(Browsers.OPERA).absolutePath)
} catch (err: FileNotFoundException) {
throw IllegalStateException("Browser Opera is not found")
}
val driver = OperaDriver(options)
driver.manage().window().fullscreen()
driver.get(htmlWrapperCreate(svgFile, dimension).toURI().toString())
val result = shoot(driver)
if (getOSName() == TypeOfOS.WINDOWS) {
Runtime.getRuntime().exec("taskkill /f /im opera.exe")
}
return result
}
}
/**
* Opening svg file and taking screenshot in IE browser.
*/
class SvgRenderByIE(settings: ProgramSettings, private val svgFile: File): SvgRender(settings) {
override fun getScreenshot(dimension: Dimension): File {
try {
System.setProperty("webdriver.ie.driver", settings.getWebDriver(Browsers.IE).absolutePath)
} catch (err: FileNotFoundException) {
throw IllegalStateException("Web driver for IE is not found. Webdriver may be downloaded from https://github.com/SeleniumHQ/selenium/wiki/InternetExplorerDriver")
}
try {
settings.getBrowser(Browsers.IE).absolutePath
} catch (err: FileNotFoundException) {
throw IllegalStateException("Browser IE is not found")
}
val driver = InternetExplorerDriver()
driver.manage().window().fullscreen()
driver.get(htmlWrapperCreate(svgFile, dimension).toURI().toString())
driver.manage().timeouts().implicitlyWait(30,TimeUnit.SECONDS);
return shoot(driver)
}
}

View File

@@ -0,0 +1,579 @@
/*
MIT License
Copyright (c) 2018 Aleksandr
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
package ru.resprojects.vqct
import ru.resprojects.ArgumentsData
import mu.KotlinLogging
import org.apache.commons.io.FileUtils
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.util.Properties
import kotlin.math.roundToInt
/**
* @author Aleksandr Aleksandrov aka mrResident
*/
private val logger = KotlinLogging.logger {}
enum class TypeOfOS {
LINUX, WINDOWS
}
enum class Browsers {
CHROME, FIREFOX, OPERA, IE
}
/**
* Get OS name from system settings.
*/
fun getOSName(): TypeOfOS {
val osName = System.getProperties().getProperty("os.name").toLowerCase().split(" ")
return if (!osName.isEmpty()) {
when (osName[0]) {
"linux" -> TypeOfOS.LINUX
"windows" -> TypeOfOS.WINDOWS
else -> {
throw IllegalStateException("Unsupported OS")
}
}
} else {
throw IllegalStateException("Unknown OS")
}
}
/**
* Generating output file.
* @param inputFile source input file with the full path and name
* @param outputDir output directory
* @param postfix addition line for output file name
* @param extension extension for output file name
* @param return output file
*/
fun outputFileConstructor(inputFile: File, outputDir: String,
postfix: String = "", extension: String = "png"): File {
return File(outputDir, "${inputFile.name.replace('.', '_')}$postfix.$extension")
}
/**
* Class for storage program settings.
* @param setupWizard run setup wizard
* @param settingsFile load custom settings file if setupWizard = false
* If setupWizard = true and settingsFile is not null then settings file
* created in custom else settings file created in default file.
*/
class ProgramSettings(setupWizard: Boolean = false, settingsFile: File? = null) {
private val prop = Properties()
private val defaultSettingsName = "vqct.properties"
init {
if (setupWizard) {
if (settingsFile != null) {
resetSettings(settingsFile, setupWizard)
} else {
resetSettings(File(defaultSettingsName), setupWizard)
}
} else {
if (settingsFile != null) {
if (settingsFile.exists()) {
loadSettings(settingsFile)
} else {
resetSettings(settingsFile)
}
} else {
if (File(defaultSettingsName).exists()) {
loadSettings()
} else {
resetSettings(File(defaultSettingsName))
}
}
}
}
/**
* Get file from settings.
* @param name settings key
* @throws FileNotFoundException if keys value is empty or file from settings value not found
*/
private fun getFile(name: String): File {
return if (prop.getProperty(name) != null && prop.getProperty(name).isNotEmpty()) {
val file = File(prop.getProperty(name))
if (file.exists()) {
file
} else {
throw FileNotFoundException("File ${file.absolutePath} not found")
}
} else {
throw FileNotFoundException("Can't get file because parameter \"$name\" in settings file is empty.")
}
}
/**
* Get browser execute file from settings.
* @param browsers type of browser
* @throws IllegalStateException if settings file is empty or not found
*/
fun getBrowser(browsers: Browsers): File {
if (prop.isEmpty) {
throw IllegalStateException("Property file is empty or not found.")
}
return when(browsers) {
Browsers.CHROME -> getFile(browsers.name)
Browsers.FIREFOX -> getFile(browsers.name)
Browsers.OPERA -> getFile(browsers.name)
Browsers.IE -> getFile(browsers.name)
}
}
/**
* Get selenium browser execute file from settings.
* @param browsers type of browser
* @throws IllegalStateException if settings file is empty or not found
*/
fun getWebDriver(browsers: Browsers): File {
if (prop.isEmpty) {
throw IllegalStateException("Property file is empty or not found.")
}
return when(browsers) {
Browsers.CHROME -> getFile("${browsers.name}_WEBDRIVER")
Browsers.FIREFOX -> getFile("${browsers.name}_WEBDRIVER")
Browsers.OPERA -> getFile("${browsers.name}_WEBDRIVER")
Browsers.IE -> getFile("${browsers.name}_WEBDRIVER")
}
}
/**
* Get ImageMagick execute file from settings.
* @throws IllegalStateException if settings file is empty or not found
* @throws FileNotFoundException if keys value is empty or file from settings value not found
*/
fun getImageMagick():File {
if (prop.isEmpty) {
throw IllegalStateException("Property file is empty or not found.")
}
return if (prop.getProperty("IMAGEMAGICK") != null && prop.getProperty("IMAGEMAGICK").isNotEmpty()) {
val file = File(prop.getProperty("IMAGEMAGICK"))
if (file.exists()) {
file
} else {
throw FileNotFoundException("File ${file.absolutePath} not found.")
}
} else {
throw FileNotFoundException("Can't get file because parameter \"IMAGEMAGICK\" in settings file is empty.")
}
}
/**
* ImageMagick compare value fuzz. Fuzz is "Fuzz Factor" using by ImageMagick to
* ignore minor differences between the two images.
*/
fun getImFuzz(): Double {
if (prop.isEmpty) {
throw IllegalStateException("Property file is empty or not found.")
}
return if (prop.getProperty("imCompareFuzzValue") != null && prop.getProperty("imCompareFuzzValue").isNotEmpty()) {
try {
prop.getProperty("imCompareFuzzValue").toDouble()
} catch (err: NumberFormatException) {
throw IllegalStateException("Value of parameter \"imCompareFuzzValue\" is not Double.")
}
} else {
throw IllegalStateException("Parameter \"imCompareFuzzValue\" in settings file is empty.")
}
}
/**
* Threshold value for count of the actual number of pixels that were masked,
* at the current "Fuzz Factor". This value used by ImageMagick for compare images.
*/
fun getImErrPixelCountThreshold(): Int {
if (prop.isEmpty) {
throw IllegalStateException("Property file is empty or not found.")
}
return if (prop.getProperty("imCompareErrCountPixelThreshold") != null && prop.getProperty("imCompareErrCountPixelThreshold").isNotEmpty()) {
try {
prop.getProperty("imCompareErrCountPixelThreshold").toInt()
} catch (err: NumberFormatException) {
throw IllegalStateException("Value of parameter \"imCompareErrCountPixelThreshold\" is not Integer.")
}
} else {
throw IllegalStateException("Parameter \"imCompareErrCountPixelThreshold\" in settings file is empty.")
}
}
/**
* Load settings from file. If file not found then created new settings file
* in current directory.
* @param settingsFile full path to settings file. If the file is not specified,
* then an attempt is made to load the settings from the vqct.properties file,
* which is in the same directory as the executable file.
*/
private fun loadSettings(settingsFile: File = File(defaultSettingsName)) {
prop.clear()
if (settingsFile.exists()) {
prop.load(settingsFile.inputStream())
} else {
throw FileNotFoundException("Settings file ${settingsFile.absolutePath} is not found.")
}
}
/**
* Reset the program settings. If the settings file is not found,
* then a new file is created, otherwise the new data is saved
* in the current file.
* @param settingsFile full path to settings file. If file is not specified,
* then settings save to default file vqct.properties, which is in the same
* directory as the executable file.
*/
private fun resetSettings(settingsFile: File, setupWizard: Boolean = false) {
prop.clear()
var str: String
var file: File
if (setupWizard) {
println("Start setup wizard. Current OS ${getOSName()}")
}
when(getOSName()) {
TypeOfOS.LINUX -> {
if (setupWizard) {
file = File("/usr/bin/google-chrome")
println("1. Path to CHROME browser [default ${if (file.exists())
file.absolutePath else "not found"}]. Enter for use default path or input yor path: ")
str = readLine()!!
prop.setProperty(Browsers.CHROME.name, if (str.isNotEmpty()) str else if (file.exists()) file.absolutePath else "")
file = File("/usr/bin/opera")
println("2. Path to OPERA browser [default ${if (file.exists())
file.absolutePath else "not found"}]. Enter for use default path or input yor path: ")
str = readLine()!!
prop.setProperty(Browsers.OPERA.name, if (str.isNotEmpty()) str else if (file.exists()) file.absolutePath else "")
file = File("/usr/bin/firefox")
println("3. Path to FIREFOX browser [default ${if (file.exists())
file.absolutePath else "not found"}]. Enter for use default path or input yor path: ")
str = readLine()!!
prop.setProperty(Browsers.FIREFOX.name, if (str.isNotEmpty()) str else if (file.exists()) file.absolutePath else "")
file = File("chromedriver")
println("4. Path to selenium webdriver for CHROME [default ${if (file.exists())
file.absolutePath else "not found"}]. Enter for use default path or input yor path: ")
str = readLine()!!
prop.setProperty("${Browsers.CHROME.name}_WEBDRIVER", if (str.isNotEmpty()) str else if (file.exists()) file.absolutePath else "")
file = File("operadriver")
println("5. Path to selenium webdriver for OPERA [default ${if (file.exists())
file.absolutePath else "not found"}]. Enter for use default path or input yor path: ")
str = readLine()!!
prop.setProperty("${Browsers.OPERA.name}_WEBDRIVER", if (str.isNotEmpty()) str else if (file.exists()) file.absolutePath else "")
file = File("geckodriver")
println("6. Path to selenium webdriver for FIREFOX [default ${if (file.exists())
file.absolutePath else "not found"}]. Enter for use default path or input yor path: ")
str = readLine()!!
prop.setProperty("${Browsers.FIREFOX.name}_WEBDRIVER", if (str.isNotEmpty()) str else if (file.exists()) file.absolutePath else "")
file = File("/usr/bin/convert")
println("7. Path to ImageMagick [default ${if (file.exists())
file.absolutePath else "not found"}]. Enter for use default path or input yor path: ")
str = readLine()!!
prop.setProperty("IMAGEMAGICK", if (str.isNotEmpty()) str else if (file.exists()) "/usr/bin" else "")
} else {
prop.setProperty(Browsers.CHROME.name, if (File("/usr/bin/google-chrome").exists()) "/usr/bin/google-chrome" else "")
prop.setProperty(Browsers.OPERA.name, if (File("/usr/bin/opera").exists()) "/usr/bin/opera" else "")
prop.setProperty(Browsers.FIREFOX.name, if (File("/usr/bin/firefox").exists()) "/usr/bin/firefox" else "")
file = File("chromedriver")
prop.setProperty("${Browsers.CHROME.name}_WEBDRIVER", if (file.exists()) file.absolutePath else "")
file = File("operadriver")
prop.setProperty("${Browsers.OPERA.name}_WEBDRIVER", if (file.exists()) file.absolutePath else "")
file = File("geckodriver")
prop.setProperty("${Browsers.FIREFOX.name}_WEBDRIVER", if (file.exists()) file.absolutePath else "")
prop.setProperty("IMAGEMAGICK", if (File("/usr/bin/convert").exists()) "/usr/bin" else "")
}
}
TypeOfOS.WINDOWS -> {
if (setupWizard) {
file = File("C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe")
if (!file.exists())
file = File("C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe")
println("1. Path to CHROME browser [default ${if (file.exists())
file.absolutePath else "not found"}]. Enter for use default path or input yor path: ")
str = readLine()!!
prop.setProperty(Browsers.CHROME.name, if (str.isNotEmpty()) str else if (file.exists()) file.absolutePath else "")
file = File("C:\\Program Files (x86)\\Opera\\launcher.exe")
if (!file.exists())
file = File("C:\\Program Files\\Opera\\launcher.exe")
println("2. Path to OPERA browser [default ${if (file.exists())
file.absolutePath else "not found"}]. Enter for use default path or input yor path: ")
str = readLine()!!
prop.setProperty(Browsers.OPERA.name, if (str.isNotEmpty()) str else if (file.exists()) file.absolutePath else "")
file = File("C:\\Program Files (x86)\\Mozilla Firefox\\firefox.exe")
if (!file.exists())
file = File("C:\\Program Files\\Mozilla Firefox\\firefox.exe")
println("3. Path to FIREFOX browser [default ${if (file.exists())
file.absolutePath else "not found"}]. Enter for use default path or input yor path: ")
str = readLine()!!
prop.setProperty(Browsers.FIREFOX.name, if (str.isNotEmpty()) str else if (file.exists()) file.absolutePath else "")
file = File("C:\\Program Files\\Internet Explorer\\iexplore.exe")
println("4. Path to IE browser [default ${if (file.exists())
file.absolutePath else "not found"}]. Enter for use default path or input yor path: ")
str = readLine()!!
prop.setProperty(Browsers.IE.name, if (str.isNotEmpty()) str else if (file.exists()) file.absolutePath else "")
file = File("chromedriver.exe")
println("5. Path to selenium webdriver for CHROME [default ${if (file.exists())
file.absolutePath else "not found"}]. Enter for use default path or input yor path: ")
str = readLine()!!
prop.setProperty("${Browsers.CHROME.name}_WEBDRIVER", if (str.isNotEmpty()) str else if (file.exists()) file.absolutePath else "")
file = File("operadriver.exe")
println("6. Path to selenium webdriver for OPERA [default ${if (file.exists())
file.absolutePath else "not found"}]. Enter for use default path or input yor path: ")
str = readLine()!!
prop.setProperty("${Browsers.OPERA.name}_WEBDRIVER", if (str.isNotEmpty()) str else if (file.exists()) file.absolutePath else "")
file = File("geckodriver.exe")
println("7. Path to selenium webdriver for FIREFOX [default ${if (file.exists())
file.absolutePath else "not found"}]. Enter for use default path or input yor path: ")
str = readLine()!!
prop.setProperty("${Browsers.FIREFOX.name}_WEBDRIVER", if (str.isNotEmpty()) str else if (file.exists()) file.absolutePath else "")
file = File("IEDriverServer.exe")
println("8. Path to selenium webdriver for IE [default ${if (file.exists())
file.absolutePath else "not found"}]. Enter for use default path or input yor path: ")
str = readLine()!!
prop.setProperty("${Browsers.IE.name}_WEBDRIVER", if (str.isNotEmpty()) str else if (file.exists()) file.absolutePath else "")
file = File("C:\\Program Files (x86)\\ImageMagick-6.9.9-Q16")
if (!file.exists())
file = File("C:\\Program Files\\ImageMagick-6.9.9-Q16")
println("9. Path to ImageMagick [default ${if (file.exists())
file.absolutePath else "not found"}]. Enter for use default path or input yor path: ")
str = readLine()!!
prop.setProperty("IMAGEMAGICK", if (str.isNotEmpty()) str else if (file.exists()) file.absolutePath else "")
} else {
file = File("C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe")
if (!file.exists())
file = File("C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe")
prop.setProperty(Browsers.CHROME.name, if (file.exists()) file.absolutePath else "")
file = File("C:\\Program Files (x86)\\Opera\\launcher.exe")
if (!file.exists())
file = File("C:\\Program Files\\Opera\\launcher.exe")
prop.setProperty(Browsers.OPERA.name, if (file.exists()) file.absolutePath else "")
file = File("C:\\Program Files (x86)\\Mozilla Firefox\\firefox.exe")
if (!file.exists())
file = File("C:\\Program Files\\Mozilla Firefox\\firefox.exe")
prop.setProperty(Browsers.FIREFOX.name, if (file.exists()) file.absolutePath else "")
file = File("C:\\Program Files\\Internet Explorer\\iexplore.exe")
prop.setProperty(Browsers.IE.name, if (file.exists()) file.absolutePath else "")
file = File("chromedriver.exe")
prop.setProperty("${Browsers.CHROME.name}_WEBDRIVER", if (file.exists()) file.absolutePath else "")
file = File("operadriver.exe")
prop.setProperty("${Browsers.OPERA.name}_WEBDRIVER", if (file.exists()) file.absolutePath else "")
file = File("geckodriver.exe")
prop.setProperty("${Browsers.FIREFOX.name}_WEBDRIVER", if (file.exists()) file.absolutePath else "")
file = File("IEDriverServer.exe")
prop.setProperty("${Browsers.IE.name}_WEBDRIVER", if (file.exists()) file.absolutePath else "")
file = File("C:\\Program Files\\ImageMagick-6.9.9-Q16")
prop.setProperty("IMAGEMAGICK", if (file.exists()) file.absolutePath else "")
}
}
}
if (setupWizard) {
println("Enter compare Fuzz value [default 15]. Enter for use default path or use own value: ")
str = readLine()!!
prop.setProperty("imCompareFuzzValue", if (str.isNotEmpty()) str else "15")
println("Enter compare compare error count pixel threshold value [default 500]. Enter for use default path or use own value: ")
str = readLine()!!
prop.setProperty("imCompareErrCountPixelThreshold", if (str.isNotEmpty()) str else "500")
} else {
prop.setProperty("imCompareFuzzValue", "15")
prop.setProperty("imCompareErrCountPixelThreshold", "500")
}
try {
prop.store(settingsFile.outputStream(), null)
} catch (err: IOException) {
throw IllegalStateException("Can't save program settings in file ${settingsFile.absolutePath}")
}
}
}
class VQCT(private val argumentsData: ArgumentsData) {
private val settings = ProgramSettings(argumentsData.isSetup, argumentsData.settingsFile)
private val imUtils = IMUtils(settings)
init {
logger.info { "Init" }
if (!((argumentsData.svgFile == null || argumentsData.basePngFile == null) && argumentsData.isSetup)) {
if (imUtils.getImageInfo(argumentsData.svgFile!!).imageFile.extension.toLowerCase() != "svg") {
throw IllegalStateException("Can not get info from ${argumentsData.svgFile!!.absolutePath}. May be file is not svg format or file is corrupted!")
}
if (imUtils.getImageInfo(argumentsData.basePngFile!!).imageFile.extension.toLowerCase() != "png") {
throw IllegalStateException("Can not get info from ${argumentsData.basePngFile!!.absolutePath}. May be file is not png format or file is corrupted!")
}
}
}
fun launch() {
if ((argumentsData.svgFile == null || argumentsData.basePngFile == null) && argumentsData.isSetup) {
return
}
logger.info { "Start processing" }
logger.info { "Checking the base png file. If the base png file is larger than the screen resolution, then need to change the picture size of the base png file to the current screen size." }
val outFile = prepareBasePNG(argumentsData.basePngFile!!)
val resultMessages = mutableListOf<String>()
var comparingResult: Boolean
argumentsData.browsers!!.forEach {
when(it.toUpperCase()) {
Browsers.CHROME.name -> {
logger.info { "1. Open the svg file in the Chrome / Chromium browser and take a screenshot from this browser." }
try {
val screenByChrome = outputFileConstructor(argumentsData.svgFile!!, argumentsData.outputDirectory!!.absolutePath, "-screen-by-chrome")
FileUtils.copyFile(
SvgRenderByChrome(settings, argumentsData.svgFile!!).getScreenshot(imUtils.getImageInfo(outFile).imageSize),
screenByChrome
)
comparingResult = comparing(outFile, screenByChrome)
logger.info { "Result of the comparing base file ${argumentsData.basePngFile!!.name} with screenshot getting from CHROME: ${if (comparingResult) "is equals" else "is not equals"}" }
resultMessages.add("Result of the comparing base file ${argumentsData.basePngFile!!.name} with screenshot getting from CHROME: ${if (comparingResult) "is equals" else "is not equals"}")
} catch (err: Exception) {
logger.error(err) { "Result of the comparing base file ${argumentsData.basePngFile!!.name} with screenshot getting from OPERA: ERROR! Info: ${err.message}" }
resultMessages.add("Result of the comparing base file ${argumentsData.basePngFile!!.name} with screenshot getting from OPERA: ERROR! For more information see logger.log file.")
}
}
Browsers.FIREFOX.name -> {
logger.info {"2. Open the svg file in the Firefox browser and take a screenshot from this browser."}
try {
val screenByFirefox = outputFileConstructor(argumentsData.svgFile!!, argumentsData.outputDirectory!!.absolutePath, "-screen-by-firefox")
FileUtils.copyFile(
SvgRenderByFirefox(settings, argumentsData.svgFile!!).getScreenshot(imUtils.getImageInfo(outFile).imageSize),
screenByFirefox
)
comparingResult = comparing(outFile, screenByFirefox)
logger.info { "Result of the comparing base file ${argumentsData.basePngFile!!.name} with screenshot getting from FIREFOX: ${if (comparingResult) "is equals" else "is not equals"}" }
resultMessages.add("Result of the comparing base file ${argumentsData.basePngFile!!.name} with screenshot getting from FIREFOX: ${if (comparingResult) "is equals" else "is not equals"}")
} catch (err: Exception) {
logger.error(err) { "Result of the comparing base file ${argumentsData.basePngFile!!.name} with screenshot getting from OPERA: ERROR! Info: ${err.message}" }
resultMessages.add("Result of the comparing base file ${argumentsData.basePngFile!!.name} with screenshot getting from OPERA: ERROR! For more information see logger.log file.")
}
}
Browsers.OPERA.name -> {
logger.info { "3. Open the svg file in the Opera browser and take a screenshot from this browser." }
try {
val screenByOpera = outputFileConstructor(argumentsData.svgFile!!, argumentsData.outputDirectory!!.absolutePath, "-screen-by-opera")
FileUtils.copyFile(
SvgRenderByOpera(settings, argumentsData.svgFile!!).getScreenshot(imUtils.getImageInfo(outFile).imageSize),
screenByOpera
)
comparingResult = comparing(outFile, screenByOpera)
logger.info { "Result of the comparing base file ${argumentsData.basePngFile!!.name} with screenshot getting from OPERA: ${if (comparingResult) "is equals" else "is not equals"}" }
resultMessages.add("Result of the comparing base file ${argumentsData.basePngFile!!.name} with screenshot getting from OPERA: ${if (comparingResult) "is equals" else "is not equals"}")
} catch (err: Exception) {
logger.error(err) { "Result of the comparing base file ${argumentsData.basePngFile!!.name} with screenshot getting from OPERA: ERROR! Info: ${err.message}" }
resultMessages.add("Result of the comparing base file ${argumentsData.basePngFile!!.name} with screenshot getting from OPERA: ERROR! For more information see logger.log file.")
}
}
Browsers.IE.name -> {
if (getOSName() == TypeOfOS.WINDOWS) {
logger.info { "4. Open the svg file in the IE browser and take a screenshot from this browser." }
try {
val screenByIE = outputFileConstructor(argumentsData.svgFile!!, argumentsData.outputDirectory!!.absolutePath, "-screen-by-ie")
FileUtils.copyFile(
SvgRenderByIE(settings, argumentsData.svgFile!!).getScreenshot(imUtils.getImageInfo(outFile).imageSize),
screenByIE
)
comparingResult = comparing(outFile, screenByIE)
logger.info { "Result of the comparing base file ${argumentsData.basePngFile!!.name} with screenshot getting from IE: ${if (comparingResult) "is equals" else "is not equals"}" }
resultMessages.add("Result of the comparing base file ${argumentsData.basePngFile!!.name} with screenshot getting from IE: ${if (comparingResult) "is equals" else "is not equals"}")
} catch (err: Exception) {
logger.error(err) { "Result of the comparing base file ${argumentsData.basePngFile!!.name} with screenshot getting from IE: ERROR! Info: ${err.message}" }
resultMessages.add("Result of the comparing base file ${argumentsData.basePngFile!!.name} with screenshot getting from IE: ERROR! For more information see logger.log file.")
}
}
}
}
}
resultMessages.add("Output directory: ${argumentsData.outputDirectory!!.absolutePath}")
resultMessages.forEach { println(it) }
}
/**
* Analized base png file and if need this file may be modified.
* If dimension of base png file more than current screen resolution, then
* base png file dimension will be resized to the current screen size.
* @param basePNG - base PNG file
* @return modified base PNG file.
*/
private fun prepareBasePNG(basePNG: File): File {
val basePNGInfo = imUtils.getImageInfo(basePNG)
val widthPercentage = if (basePNGInfo.imageSize.getWidth() > getCurrentScreenResolution().getWidth()) {
(((getCurrentScreenResolution().getWidth() - 100) / (basePNGInfo.imageSize.getWidth() * 1.0f)) * 100).roundToInt()
} else {
100
}
val heightPercentage = if (basePNGInfo.imageSize.getHeight() > getCurrentScreenResolution().getHeight()) {
(((getCurrentScreenResolution().getHeight() - 100) / (basePNGInfo.imageSize.getHeight() * 1.0f)) * 100).roundToInt()
} else {
100
}
val percentage = when {
widthPercentage < heightPercentage -> widthPercentage
widthPercentage > heightPercentage -> heightPercentage
widthPercentage == heightPercentage -> widthPercentage
else -> 100
}
FileUtils.forceMkdir(argumentsData.outputDirectory!!)
return when {
percentage == 100 -> {
logger.info { "Size picture of the base png file staying without changes." }
basePNG
}
percentage != 100 -> {
val outFile = outputFileConstructor(basePNG, argumentsData.outputDirectory!!.absolutePath, postfix = "-resize")
if (imUtils.resizeImage(
basePNG,
outFile,
width = (basePNGInfo.imageSize.getWidth() * percentage / (100 * 1.0f)).roundToInt(),
height = (basePNGInfo.imageSize.getHeight() * percentage / (100 * 1.0f)).roundToInt()
)) {
logger.info { "Size picture of the base png file changed from ${basePNGInfo.imageSize} to the ${imUtils.getImageInfo(outFile).imageSize}" }
outFile
} else {
logger.info { "Size picture of the base png file staying without changes." }
basePNG
}
}
else -> {
logger.info { "Size picture of the base png file staying without changes." }
basePNG
}
}
}
/**
* Comparing base image with screenshots from browsers.
* @param basePNG
* @param screenshot
* @return result png file.
*/
private fun comparing(basePNG: File, screenshot: File): Boolean {
val result = imUtils.compareImage(
basePNG,
screenshot,
outputFileConstructor(screenshot, argumentsData.outputDirectory!!.absolutePath, "-compare-result")
)
logger.info { "Comparing base png file ${basePNG.absolutePath} with screenshot file ${screenshot.absolutePath}. Current absolute error count (number of different pixels) is equals to $result (current threshold = ${settings.getImErrPixelCountThreshold()})" }
return result in 0..settings.getImErrPixelCountThreshold()
}
}

View File

@@ -0,0 +1,35 @@
# SLF4J's SimpleLogger configuration file
# Simple implementation of Logger that sends all enabled log messages, for all defined loggers, to System.err.
org.slf4j.simpleLogger.logFile=logger.log
# Default logging detail level for all instances of SimpleLogger.
# Must be one of ("trace", "debug", "info", "warn", or "error").
# If not specified, defaults to "info".
# org.slf4j.simpleLogger.defaultLogLevel=debug
# Logging detail level for a SimpleLogger instance named "xxxxx".
# Must be one of ("trace", "debug", "info", "warn", or "error").
# If not specified, the default logging detail level is used.
# Set to true if you want the current date and time to be included in output messages.
# Default is false, and will output the number of milliseconds elapsed since startup.
org.slf4j.simpleLogger.showDateTime=true
# The date and time format to be used in the output messages.
# The pattern describing the date and time format is the same that is used in java.text.SimpleDateFormat.
# If the format is not specified or is invalid, the default format is used.
# The default format is yyyy-MM-dd HH:mm:ss:SSS Z.
org.slf4j.simpleLogger.dateTimeFormat=dd-MM-yyyy HH:mm:ss
# Set to true if you want to output the current thread name.
# Defaults to true.
org.slf4j.simpleLogger.showThreadName=false
# Set to true if you want the Logger instance name to be included in output messages.
# Defaults to true.
org.slf4j.simpleLogger.showLogName=false
# Set to true if you want the last component of the name to be included in output messages.
# Defaults to false.
#org.slf4j.simpleLogger.showShortLogName=false

View File

@@ -0,0 +1,505 @@
/*
MIT License
Copyright (c) 2018 Aleksandr
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
package ru.resprojects
import ru.resprojects.vqct.Browsers
import ru.resprojects.vqct.ProgramSettings
import org.junit.Assume
import org.openqa.selenium.Dimension
import ru.resprojects.vqct.IMUtils
import ru.resprojects.vqct.ImageInfo
import java.io.*
import java.util.*
import kotlin.test.*
/**
* @author Aleksandr Aleksandrov aka mrResident
*/
class MainTest {
private val testSvg = Thread.currentThread().contextClassLoader.getResource("gallardo.svg").toURI().path
private val testPng = Thread.currentThread().contextClassLoader.getResource("gallardo.png").toURI().path
private val testPng1 = Thread.currentThread().contextClassLoader.getResource("flag_top.png").toURI().path
private val testProperties = Thread.currentThread().contextClassLoader.getResource("vqct.properties").toURI().path
private val testFile = Thread.currentThread().contextClassLoader.getResource("simplelogger.properties").toURI().path
private val testOtherFile = Thread.currentThread().contextClassLoader.getResource("logger.log").toURI().path
private val launchTest = try {
ProgramSettings().getBrowser(Browsers.CHROME)
true
} catch (err: Exception) {
false
}
private val errorMessage1 = "ERROR: Not specified SVG file or Base PNG file! Must be specified minimum two arguments <path_to_svg_file> <path_to_png_file>"
@Test
fun `run program without argument's`() {
Assume.assumeTrue(launchTest)
try {
main(arrayOf())
} catch (err: Exception) {
assertEquals(usageMessage, err.message)
}
}
@Test
fun `run program with minimal arguments`() {
Assume.assumeTrue(launchTest)
main(arrayOf(testSvg, testPng))
}
@Test
fun `program stopped with error message`() {
val baOut = ByteArrayOutputStream()
val out = PrintStream(baOut)
val oldout = System.out
val olderr = System.err
System.setOut(out)
System.setErr(out)
main(arrayOf(testSvg, "123.png"))
System.setOut(oldout)
System.setErr(olderr)
val s = String(baOut.toByteArray())
assertTrue {
s.contains("Working stopped with ERROR!")
}
baOut.reset()
System.setOut(out)
System.setErr(out)
main(arrayOf("--load-settings=$testProperties",testSvg, testPng))
System.setOut(oldout)
System.setErr(olderr)
val s1 = String(baOut.toByteArray())
assertTrue {
s1.contains("Working stopped with ERROR! For more information see logger.log file!")
}
}
@Test
fun `run programm with --setup key`() {
val `in` = ByteArrayInputStream("\n\n\n\n\n\n\n\n\n".toByteArray())
val old = System.`in`
System.setIn(`in`)
main(arrayOf("--setup"))
System.setIn(old)
}
@Test
fun `run programm with --setup key and minimal arguments`() {
Assume.assumeTrue(launchTest)
val `in` = ByteArrayInputStream("\n\n\n\n\n\n\n\n\n".toByteArray())
val old = System.`in`
System.setIn(`in`)
main(arrayOf("--setup", testSvg, testPng))
System.setIn(old)
}
@Test
fun `run program with --help key`() {
val baOut = ByteArrayOutputStream()
val out = PrintStream(baOut)
val oldout = System.out
val olderr = System.err
System.setOut(out)
System.setErr(out)
main(arrayOf("--help"))
System.setOut(oldout)
System.setErr(olderr)
val s = String(baOut.toByteArray())
assertTrue {
s.contains(usageMessage)
}
}
@Test(expected = IllegalArgumentException::class)
fun `throws exception if input data for parseArguments is empty`() {
parseArguments(arrayOf())
}
@Test
fun `parseArguments return correct data`() {
val argData = parseArguments(arrayOf("--setup", "--load-settings=$testProperties", "--browsers=chrome,opera", "--out=/out/dir", testSvg, testPng))
assertNotNull(argData.svgFile)
assertNotNull(argData.basePngFile)
assertNotNull(argData.outputDirectory)
assertNotNull(argData.browsers)
assertNotNull(argData.settingsFile)
assertTrue {
argData.isSetup
}
}
@Test
fun `parseArguments return correct data if used key --out with short path or fullt path`() {
val argData = parseArguments(arrayOf("--out=/out/dir", testSvg, testPng))
assertNotNull(argData.outputDirectory)
val argData1 = parseArguments(arrayOf("--out=dir/path", testSvg, testPng))
assertNotNull(argData1.outputDirectory)
val argData2 = parseArguments(arrayOf("--out=dir/path/", testSvg, testPng))
assertNotNull(argData2.outputDirectory)
}
@Test
fun `parseArguments return correct data if input only one key --setup`() {
val argData = parseArguments(arrayOf("--setup"))
assertNull(argData.svgFile)
assertNull(argData.basePngFile)
assertNull(argData.outputDirectory)
assertNull(argData.browsers)
assertNull(argData.settingsFile)
assertTrue {
argData.isSetup
}
}
@Test
fun `parseArguments throws if not specified minimal arguments`() {
try {
parseArguments(arrayOf(testSvg))
} catch (err: Exception) {
assertEquals(errorMessage1, err.message)
}
try {
parseArguments(arrayOf(testPng))
} catch (err: Exception) {
assertEquals(errorMessage1, err.message)
}
try {
parseArguments(arrayOf("123.svg", testPng))
} catch (err: Exception) {
assertTrue {
err.message!!.startsWith("ERROR: File")
}
}
try {
parseArguments(arrayOf(testSvg, "123.png"))
} catch (err: Exception) {
assertTrue {
err.message!!.startsWith("ERROR: File")
}
}
}
@Test
fun `parseArguments throws if key value --load-settings is invalid`() {
try {
parseArguments(arrayOf("--load-settings=$testOtherFile", testSvg, testPng))
} catch (err: Exception) {
assertTrue {
err.message!!.startsWith("ERROR: Invalid properties file")
}
}
try {
parseArguments(arrayOf("--load-settings=/123/file.properties", testSvg, testPng))
} catch (err: Exception) {
assertTrue {
err.message!!.startsWith("ERROR: Properties file")
}
}
try {
parseArguments(arrayOf("--load-settings", testSvg, testPng))
} catch (err: Exception) {
assertTrue {
err.message!!.startsWith("ERROR: Invalid key value")
}
}
try {
parseArguments(arrayOf("--load-settings=", testSvg, testPng))
} catch (err: Exception) {
assertTrue {
err.message!!.startsWith("ERROR: Invalid key value")
}
}
}
@Test
fun `parseArguments throws if key value --browsers is invalid`() {
try {
parseArguments(arrayOf("--browsers", testSvg, testPng))
} catch (err: Exception) {
assertTrue {
err.message!!.startsWith("ERROR: Invalid key value --browsers.")
}
}
try {
parseArguments(arrayOf("--browsers=", testSvg, testPng))
} catch (err: Exception) {
assertTrue {
err.message!!.startsWith("ERROR: Invalid key value --browsers.")
}
}
try {
parseArguments(arrayOf("--browsers=123,chrome", testSvg, testPng))
} catch (err: Exception) {
assertTrue {
err.message!!.startsWith("ERROR: Invalid value")
}
}
}
@Test
fun `parseArguments throws if key value --out is invalid`() {
try {
parseArguments(arrayOf("--out", testSvg, testPng))
} catch (err: Exception) {
assertTrue {
err.message!!.startsWith("ERROR: Invalid key value --out.")
}
}
try {
parseArguments(arrayOf("--out=", testSvg, testPng))
} catch (err: Exception) {
assertTrue {
err.message!!.startsWith("ERROR: Invalid key value --out.")
}
}
}
@Test
fun `parseArguments throws if not specified minimal arguments with key --setup`() {
try {
parseArguments(arrayOf(testSvg, "--setup"))
} catch (err: Exception) {
assertEquals(errorMessage1, err.message)
}
try {
parseArguments(arrayOf("--setup", testPng))
} catch (err: Exception) {
assertEquals(errorMessage1, err.message)
}
}
@Test
fun `parseArguments throws if unexpected key value`() {
try {
parseArguments(arrayOf(testSvg, testPng, "-key"))
} catch (err: Exception) {
println(err.message)
assertTrue {
err.message!!.startsWith("Invalid input", ignoreCase = true)
}
}
}
@Test
fun `create settings file`() {
ProgramSettings()
}
@Test
fun `load settings from custom file`() {
val file = File("custom.properties")
val prop = Properties()
prop.setProperty(Browsers.CHROME.name, "")
prop.store(file.outputStream(), null)
ProgramSettings(false, File("custom.properties"))
}
@Test
fun `auto creating settings in custom file`() {
ProgramSettings(false, File("custom_2.properties"))
}
@Test
fun `throws exception if parameter value is not found`() {
val file = File("vqct_test.properties")
val prop = Properties()
prop.setProperty("any_data", "any_data")
prop.store(file.outputStream(), null)
val ps = ProgramSettings(false, file)
try {
ps.getBrowser(Browsers.CHROME)
} catch (err: Exception) {
assertEquals("Can't get file because parameter \"CHROME\" in settings file is empty.", err.message.toString())
}
}
@Test(expected = FileNotFoundException::class)
fun `throws exception if browser not found`() {
val file = File("vqct_test.properties")
val prop = Properties()
prop.setProperty(Browsers.CHROME.name, "")
prop.store(file.outputStream(), null)
val ps = ProgramSettings(false, file)
ps.getBrowser(Browsers.CHROME)
}
@Test(expected = FileNotFoundException::class)
fun `throws exception if webdriver not found`() {
val file = File("vqct_test.properties")
val prop = Properties()
prop.setProperty("${Browsers.CHROME.name}_WEBDRIVER", "")
prop.store(file.outputStream(), null)
val ps = ProgramSettings(false, file)
ps.getWebDriver(Browsers.CHROME)
}
@Test(expected = FileNotFoundException::class)
fun `throws exception if imagemagick not found`() {
val file = File("vqct_test.properties")
val prop = Properties()
prop.setProperty("IMAGEMAGICK", "")
prop.store(file.outputStream(), null)
val ps = ProgramSettings(false, file)
ps.getImageMagick()
}
@Test(expected = IllegalStateException::class)
fun `throws exception while save settings file`() {
val file = File("/prop.properties")
ProgramSettings(false, file)
}
@Test
fun `Get default imCompareFuzzValue value`() {
val ps = ProgramSettings()
assertEquals(15.0, ps.getImFuzz())
}
@Test
fun `Get default imCompareErrCountPixelThreshold value`() {
val ps = ProgramSettings()
assertEquals(500, ps.getImErrPixelCountThreshold())
}
@Test
fun `Throws exception while get value from parameter imCompareFuzzValue`() {
val file = File("vqct_test.properties")
val prop = Properties()
prop.setProperty("imCompareFuzzValue", "12d")
prop.store(file.outputStream(), null)
val ps = ProgramSettings(false, file)
try {
ps.getImFuzz()
} catch (err: IllegalStateException) {
assertEquals("Value of parameter \"imCompareFuzzValue\" is not Double.", err.message.toString())
}
}
@Test
fun `Throws exception while get value from parameter imCompareErrCountPixelThreshold`() {
val file = File("vqct_test.properties")
val prop = Properties()
prop.setProperty("imCompareErrCountPixelThreshold", "12d")
prop.store(file.outputStream(), null)
val ps = ProgramSettings(false, file)
try {
ps.getImErrPixelCountThreshold()
} catch (err: IllegalStateException) {
assertEquals("Value of parameter \"imCompareErrCountPixelThreshold\" is not Integer.", err.message.toString())
}
}
@Test
fun `Getting Image Info from file`() {
val imageUtils = IMUtils(ProgramSettings())
val imageInfo = imageUtils.getImageInfo(File(testPng))
println("Image size = ${imageInfo.imageSize}")
println("Image quality = ${imageInfo.imageQuality}")
println("Image format = ${imageInfo.imageFormat}")
println("Image file path = ${imageInfo.imageFile.absolutePath}")
}
@Test(expected = FileNotFoundException::class)
fun `Throws exception while getting image info if file not exists`() {
IMUtils(ProgramSettings()).getImageInfo(File("/123"))
}
@Test(expected = IllegalStateException::class)
fun `Throws exception while getting image info if file not image`() {
IMUtils(ProgramSettings()).getImageInfo(File(testFile))
}
@Test(expected = IllegalStateException::class)
fun `Throws exception while getting Image Info`() {
ImageInfo(listOf())
}
@Test
fun `Cropping image`() {
val imUtils = IMUtils(ProgramSettings())
val testPngFile = File(testPng)
assertEquals(true, imUtils.cropImage(
testPngFile,
File("testpng_cropping.png"),
imUtils.getImageInfo(testPngFile).imageSize,
-100, -100
))
}
@Test
fun `Error while cropping image`() {
val imUtils = IMUtils(ProgramSettings())
val testPngFile = File(testFile)
assertEquals(false, imUtils.cropImage(
testPngFile,
File("testpng_cropping.png"),
Dimension(2048, 2048),
0, 0
))
}
@Test
fun `Trimming image`() {
val imUtils = IMUtils(ProgramSettings())
val testPngFile = File(testPng1)
assertEquals(true, imUtils.trimImage(
testPngFile,
File("testpng1_trimming.png")
))
}
@Test
fun `Error while trimming image`() {
val imUtils = IMUtils(ProgramSettings())
val testPngFile = File(testFile)
assertEquals(false, imUtils.trimImage(
testPngFile,
File("testpng1_trimming.png")
))
}
@Test
fun `Resizing image`() {
val imUtils = IMUtils(ProgramSettings())
val testPngFile = File(testPng)
assertEquals(true, imUtils.resizeImage(
testPngFile,
File("testpng_resising.png"),
256, 256
))
}
@Test
fun `Error while resizing image`() {
val imUtils = IMUtils(ProgramSettings())
val testPngFile = File(testFile)
assertEquals(false, imUtils.resizeImage(
testPngFile,
File("testpng_resising.png"),
256, 256
))
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1013 1308.9" style="enable-background:new 0 0 1013 1308.9;" xml:space="preserve">
<path d="M415.8,332.5l3,1.5c1,0.5,2.1-0.2,2.1-1.3v-42.2c0-0.5-0.3-1-0.7-1.3l-0.9-0.5c-20.2-11.3-36.8-17.9-54-21.5l-2.5-0.5
c-0.9-0.2-1.7,0.5-1.7,1.4v38.2c0,0.9-0.7,1.5-1.6,1.4c-14-1.5-27.9-1.9-43-1.1c-0.8,0-1.5-0.6-1.5-1.4v-41.1c0-0.8-0.7-1.5-1.5-1.4
l-2.1,0.1c-16.5,0.9-33.5,2.7-53.4,5.7l-1.6,0.3c-0.7,0.1-1.2,0.7-1.2,1.4V315c0,0.7-0.5,1.3-1.1,1.4c-7.6,1.7-15.3,3.5-22.8,5.2
c-6.7,1.5-13.6,3.1-20.5,4.6c-0.9,0.2-1.8-0.5-1.8-1.4v-46.2c0-0.9-0.8-1.6-1.7-1.4l-2.3,0.4c-19.7,3-36.4,4.8-52.7,5.7l-1.9,0.1
c-0.8,0-1.4,0.7-1.4,1.4v50.1c0,0.8-0.6,1.4-1.4,1.4c-15.3,0.8-29.4,0.4-43.5-1.2c-0.7-0.1-1.3-0.7-1.3-1.4v-53
c0-0.7-0.5-1.3-1.1-1.4l-1.5-0.3c-16.7-3.7-33.2-10.3-52-20.8l-3.1-1.7c-1-0.5-2.1,0.2-2.1,1.3v62.8c0,0.5,0.3,1,0.8,1.3l1,0.5
c16.6,8.3,32.6,13.9,50.1,17.7c0.7,0.1,1.1,0.7,1.1,1.4v46.7c0,0.9-0.8,1.6-1.7,1.4c-16.2-2.8-30.8-7.2-46.4-14.2l-2.9-1.3
c-1-0.4-2,0.3-2,1.3v62.5c0,0.6,0.4,1.1,0.9,1.3l1.2,0.5c18.1,7,34.8,10.9,54,12.7l2.2,0.2c0.8,0.1,1.6-0.6,1.6-1.4v-52.7
c0-0.8,0.7-1.5,1.5-1.4c4.7,0.3,9.3,0.4,14,0.4c9.4,0,18.9-0.5,29-1.6c0.8-0.1,1.6,0.6,1.6,1.4v49.9c0,0.9,0.8,1.6,1.7,1.4l2.5-0.5
c16.5-3.6,33.5-8.2,53.3-14.5l1.3-0.4c0.6-0.2,1-0.7,1-1.4V384c0-0.6,0.4-1.2,1.1-1.4c7.7-2.1,15.5-4.3,23-6.5
c6.6-1.9,13.5-3.8,20.3-5.7c0.9-0.3,1.8,0.4,1.8,1.4v43.3c0,1,0.9,1.7,1.9,1.4l2.7-0.9c19.7-6.2,36.4-10.8,52.7-14.3l1.5-0.3
c0.7-0.1,1.1-0.7,1.1-1.4v-41.4c0-0.7,0.6-1.4,1.3-1.4c15.3-1.7,29.4-2.1,43.5-1.2c0.8,0,1.4,0.7,1.4,1.4v38.4
c0,0.7,0.6,1.4,1.3,1.4l1.8,0.2c18.5,1.7,34.6,5.4,52,12.2l2.8,1.1c0.9,0.4,2-0.3,2-1.3v-42c0-0.6-0.3-1.1-0.9-1.3l-1.1-0.5
c-16.8-7.4-32.3-12.1-49.9-15c-0.7-0.1-1.2-0.7-1.2-1.4v-31.6c0-0.9,0.9-1.6,1.8-1.4C385.8,319.5,400.2,324.7,415.8,332.5z
M306.8,351.7c-13.5,2.3-27.4,5.3-43.2,9.3c-0.9,0.2-1.8-0.5-1.8-1.4v-37.3c0-0.7,0.5-1.3,1.2-1.4c15.9-3.2,29.9-5.5,43.4-7.2
c0.9-0.1,1.6,0.6,1.6,1.4v35.1C308,351,307.5,351.6,306.8,351.7z M200.8,378.4c-15.9,4.1-29.9,7.2-43.4,9.6
c-0.9,0.2-1.7-0.5-1.7-1.4v-43.3c0-0.7,0.5-1.3,1.3-1.4c13.5-1.6,27.4-3.9,43.1-7c0.9-0.2,1.7,0.5,1.7,1.4V377
C201.9,377.7,201.5,378.3,200.8,378.4z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 826 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 333 KiB

View File

@@ -0,0 +1 @@
18-03-2018 00:46:06 INFO Init

View File

@@ -0,0 +1,40 @@
# SLF4J's SimpleLogger configuration file
# Simple implementation of Logger that sends all enabled log messages, for all defined loggers, to System.err.
org.slf4j.simpleLogger.logFile=logger_test.log
# Default logging detail level for all instances of SimpleLogger.
# Must be one of ("trace", "debug", "info", "warn", or "error").
# If not specified, defaults to "info".
org.slf4j.simpleLogger.defaultLogLevel=debug
# Logging detail level for a SimpleLogger instance named "xxxxx".
# Must be one of ("trace", "debug", "info", "warn", or "error").
# If not specified, the default logging detail level is used.
org.slf4j.simpleLogger.log.debug=debug
org.slf4j.simpleLogger.log.error=error
org.slf4j.simpleLogger.log.info=info
org.slf4j.simpleLogger.log.trace=trace
org.slf4j.simpleLogger.log.warn=warn
# Set to true if you want the current date and time to be included in output messages.
# Default is false, and will output the number of milliseconds elapsed since startup.
org.slf4j.simpleLogger.showDateTime=true
# The date and time format to be used in the output messages.
# The pattern describing the date and time format is the same that is used in java.text.SimpleDateFormat.
# If the format is not specified or is invalid, the default format is used.
# The default format is yyyy-MM-dd HH:mm:ss:SSS Z.
org.slf4j.simpleLogger.dateTimeFormat=dd-MM-yyyy HH:mm:ss
# Set to true if you want to output the current thread name.
# Defaults to true.
org.slf4j.simpleLogger.showThreadName=true
# Set to true if you want the Logger instance name to be included in output messages.
# Defaults to true.
org.slf4j.simpleLogger.showLogName=true
# Set to true if you want the last component of the name to be included in output messages.
# Defaults to false.
org.slf4j.simpleLogger.showShortLogName=true

View File

@@ -0,0 +1,10 @@
#Sun Mar 18 01:36:25 MSK 2018
IMAGEMAGICK=
CHROME=/usr/bin/google-chrome
FIREFOX_WEBDRIVER=/media/mrresident/archive/Nextcloud/projects/gitlab/visual-quality-control-tool/geckodriver
imCompareFuzzValue=15
CHROME_WEBDRIVER=/media/mrresident/archive/Nextcloud/projects/gitlab/visual-quality-control-tool/chromedriver
imCompareErrCountPixelThreshold=500
OPERA=/usr/bin/opera
FIREFOX=/usr/bin/firefox
OPERA_WEBDRIVER=/media/mrresident/archive/Nextcloud/projects/gitlab/visual-quality-control-tool/operadriver