appium_failure_helper 1.1.5 → 1.1.6
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 +4 -4
- data/lib/appium_failure_helper/analyzer.rb +42 -17
- data/lib/appium_failure_helper/element_repository.rb +16 -24
- data/lib/appium_failure_helper/handler.rb +40 -10
- data/lib/appium_failure_helper/report_generator.rb +11 -52
- data/lib/appium_failure_helper/source_code_analyzer.rb +0 -8
- data/lib/appium_failure_helper/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 78e35ec0789690a1c581b094e7f1a1bc8cf87ffd7b5775b58805bc3d0954e2c0
|
4
|
+
data.tar.gz: 62f6e3a7a2338ec85b6f5c37064509d45534cf55197e0a791048763aac85e044
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3f57d410bc16465a452693cd5c2211fcc159def18c39b58d6de140be38e37c78f64b625f31844f77e566b5c222fbcd1d5c79b64408fad427038f360f75a4a07a
|
7
|
+
data.tar.gz: e7322ab59bde3d4fa00551573ef5fadb6e8091f7b374b9170dc1fddac247908393b442be3c0b72ef3fb144e2d680547631a946d4711cea03d3ff7029d2536a42
|
@@ -1,30 +1,55 @@
|
|
1
1
|
module AppiumFailureHelper
|
2
2
|
module Analyzer
|
3
3
|
def self.triage_error(exception)
|
4
|
-
|
5
|
-
|
4
|
+
case exception
|
5
|
+
when Selenium::WebDriver::Error::NoSuchElementError,
|
6
|
+
Selenium::WebDriver::Error::TimeoutError,
|
7
|
+
Selenium::WebDriver::Error::UnknownCommandError
|
6
8
|
:locator_issue
|
9
|
+
when Selenium::WebDriver::Error::ElementNotInteractableError
|
10
|
+
:visibility_issue
|
11
|
+
when Selenium::WebDriver::Error::StaleElementReferenceError
|
12
|
+
:stale_element_issue
|
13
|
+
when defined?(RSpec::Expectations::ExpectationNotMetError) ? RSpec::Expectations::ExpectationNotMetError : Class.new
|
14
|
+
:assertion_failure
|
15
|
+
when NoMethodError, NameError, ArgumentError, TypeError
|
16
|
+
:ruby_code_issue
|
17
|
+
when Selenium::WebDriver::Error::SessionNotCreatedError, Errno::ECONNREFUSED
|
18
|
+
:session_startup_issue
|
19
|
+
when Selenium::WebDriver::Error::WebDriverError
|
20
|
+
return :app_crash_issue if exception.message.include?('session deleted because of page crash')
|
21
|
+
:unknown_appium_issue
|
7
22
|
else
|
8
|
-
:
|
23
|
+
:unknown_issue
|
9
24
|
end
|
10
25
|
end
|
11
26
|
|
12
27
|
def self.extract_failure_details(exception)
|
13
|
-
message = exception.message
|
28
|
+
message = exception.message.to_s
|
14
29
|
info = {}
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
30
|
+
patterns = [
|
31
|
+
/using "([^"]+)" with value "([^"]+)"/,
|
32
|
+
/element with locator ['"]?(#?\w+)['"]?/i,
|
33
|
+
/(?:could not be found|cannot find element) using (.+?)=['"]?([^'"]+)['"]?/i,
|
34
|
+
/no such element: Unable to locate element: {"method":"([^"]+)","selector":"([^"]+)"}/i,
|
35
|
+
]
|
36
|
+
patterns.each do |pattern|
|
37
|
+
match = message.match(pattern)
|
38
|
+
if match
|
39
|
+
if match.captures.size == 2
|
40
|
+
info[:selector_type], info[:selector_value] = match.captures.map(&:strip)
|
41
|
+
else
|
42
|
+
info[:selector_value] = match.captures.first.strip
|
43
|
+
info[:selector_type] = 'logical_name'
|
44
|
+
end
|
45
|
+
return info
|
46
|
+
end
|
22
47
|
end
|
23
48
|
info
|
24
49
|
end
|
25
|
-
|
50
|
+
|
26
51
|
def self.find_de_para_match(failed_info, element_map)
|
27
|
-
failed_value = failed_info[:selector_value].to_s
|
52
|
+
failed_value = (failed_info || {})[:selector_value].to_s
|
28
53
|
return nil if failed_value.empty?
|
29
54
|
logical_name_key = failed_value.gsub(/^#/, '')
|
30
55
|
if element_map.key?(logical_name_key)
|
@@ -32,7 +57,7 @@ module AppiumFailureHelper
|
|
32
57
|
end
|
33
58
|
cleaned_failed_locator = failed_value.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
|
34
59
|
element_map.each do |name, locator_info|
|
35
|
-
mapped_locator = locator_info
|
60
|
+
mapped_locator = (locator_info || {})['valor'].to_s
|
36
61
|
cleaned_mapped_locator = mapped_locator.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
|
37
62
|
distance = DidYouMean::Levenshtein.distance(cleaned_failed_locator, cleaned_mapped_locator)
|
38
63
|
max_len = [cleaned_failed_locator.length, cleaned_mapped_locator.length].max
|
@@ -46,14 +71,14 @@ module AppiumFailureHelper
|
|
46
71
|
end
|
47
72
|
|
48
73
|
def self.find_similar_elements(failed_info, all_page_suggestions)
|
49
|
-
failed_locator_value = failed_info[:selector_value]
|
50
|
-
failed_locator_type = failed_info[:selector_type]
|
74
|
+
failed_locator_value = (failed_info || {})[:selector_value]
|
75
|
+
failed_locator_type = (failed_info || {})[:selector_type]
|
51
76
|
return [] unless failed_locator_value && failed_locator_type
|
52
77
|
normalized_failed_type = failed_locator_type.to_s.downcase.include?('id') ? 'id' : failed_locator_type.to_s
|
53
78
|
cleaned_failed_locator = failed_locator_value.to_s.gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
|
54
79
|
similarities = []
|
55
80
|
all_page_suggestions.each do |suggestion|
|
56
|
-
candidate_locator = suggestion[:locators].find { |loc| loc[:strategy] == normalized_failed_type }
|
81
|
+
candidate_locator = (suggestion[:locators] || []).find { |loc| loc[:strategy] == normalized_failed_type }
|
57
82
|
next unless candidate_locator
|
58
83
|
cleaned_candidate_locator = candidate_locator[:locator].gsub(/[:\-\/@=\[\]'"()]/, ' ').gsub(/\s+/, ' ').downcase.strip
|
59
84
|
distance = DidYouMean::Levenshtein.distance(cleaned_failed_locator, cleaned_candidate_locator)
|
@@ -1,50 +1,42 @@
|
|
1
|
+
# lib/appium_failure_helper/element_repository.rb
|
1
2
|
module AppiumFailureHelper
|
2
3
|
module ElementRepository
|
3
|
-
|
4
4
|
def self.load_all
|
5
|
-
|
6
|
-
|
5
|
+
config = AppiumFailureHelper.configuration
|
6
|
+
base_path = config.elements_path
|
7
|
+
elements_map = load_from_ruby_file(base_path, config.elements_ruby_file)
|
8
|
+
elements_map.merge!(load_all_from_yaml(base_path))
|
9
|
+
Utils.logger.info("Mapa de elementos carregado da pasta '#{base_path}'. Total: #{elements_map.size}.")
|
7
10
|
elements_map
|
8
11
|
end
|
9
12
|
|
10
13
|
private
|
11
14
|
|
12
|
-
def self.load_from_ruby_file
|
15
|
+
def self.load_from_ruby_file(base_path, filename)
|
13
16
|
map = {}
|
14
|
-
|
15
|
-
file_path = File.join(Dir.pwd, config.elements_path, config.elements_ruby_file)
|
16
|
-
|
17
|
+
file_path = File.join(base_path, filename)
|
17
18
|
return map unless File.exist?(file_path)
|
18
|
-
|
19
19
|
begin
|
20
|
-
require file_path
|
20
|
+
require File.expand_path(file_path)
|
21
21
|
instance = OnboardingElementLists.new
|
22
22
|
unless instance.respond_to?(:elements)
|
23
|
-
Utils.logger.warn("AVISO: A classe
|
23
|
+
Utils.logger.warn("AVISO: A classe #{instance.class} não expõe um `attr_reader :elements`.")
|
24
24
|
return map
|
25
25
|
end
|
26
26
|
instance.elements.each do |key, value|
|
27
|
-
|
28
|
-
if valor.is_a?(Hash)
|
29
|
-
valor_final = valor['valor'] || valor['value'] || valor
|
30
|
-
else
|
31
|
-
valor_final = valor
|
32
|
-
end
|
33
|
-
map[key.to_s] = { 'tipoBusca' => value[0], 'valor' => valor_final }
|
27
|
+
map[key.to_s] = { 'tipoBusca' => value[0], 'valor' => value[1] }
|
34
28
|
end
|
35
29
|
rescue => e
|
36
|
-
Utils.logger.warn("AVISO: Erro ao processar o arquivo #{file_path}: #{e.message}")
|
30
|
+
Utils.logger.warn("AVISO: Erro ao processar o arquivo Ruby #{file_path}: #{e.message}")
|
37
31
|
end
|
38
|
-
|
39
32
|
map
|
40
33
|
end
|
41
34
|
|
42
|
-
def self.load_all_from_yaml
|
35
|
+
def self.load_all_from_yaml(base_path)
|
43
36
|
elements_map = {}
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
Dir.glob(glob_path).each do |file|
|
37
|
+
glob_path = File.join(base_path, '**', '*.yaml')
|
38
|
+
files_found = Dir.glob(glob_path)
|
39
|
+
files_found.each do |file|
|
48
40
|
next if file.include?('reports_failure')
|
49
41
|
begin
|
50
42
|
data = YAML.load_file(file)
|
@@ -1,4 +1,3 @@
|
|
1
|
-
# lib/appium_failure_helper/handler.rb
|
2
1
|
module AppiumFailureHelper
|
3
2
|
class Handler
|
4
3
|
def self.call(driver, exception)
|
@@ -6,7 +5,10 @@ module AppiumFailureHelper
|
|
6
5
|
end
|
7
6
|
|
8
7
|
def initialize(driver, exception)
|
9
|
-
@driver = driver
|
8
|
+
@driver = driver
|
9
|
+
@exception = exception
|
10
|
+
@timestamp = Time.now.strftime('%Y%m%d_%H%M%S')
|
11
|
+
@output_folder = "reports_failure/failure_#{@timestamp}"
|
10
12
|
end
|
11
13
|
|
12
14
|
def call
|
@@ -17,26 +19,53 @@ module AppiumFailureHelper
|
|
17
19
|
end
|
18
20
|
|
19
21
|
FileUtils.mkdir_p(@output_folder)
|
22
|
+
|
20
23
|
triage_result = Analyzer.triage_error(@exception)
|
21
24
|
|
22
|
-
report_data = {
|
25
|
+
report_data = {
|
26
|
+
exception: @exception,
|
27
|
+
triage_result: triage_result,
|
28
|
+
timestamp: @timestamp,
|
29
|
+
platform: @driver.capabilities['platformName'] || @driver.capabilities[:platformName] || 'unknown',
|
30
|
+
screenshot_base64: @driver.screenshot_as(:base64)
|
31
|
+
}
|
23
32
|
|
24
33
|
if triage_result == :locator_issue
|
25
34
|
page_source = @driver.page_source
|
26
|
-
|
35
|
+
doc = Nokogiri::XML(page_source)
|
27
36
|
|
28
|
-
|
37
|
+
failed_info = Analyzer.extract_failure_details(@exception) || {}
|
38
|
+
if failed_info.empty?
|
39
|
+
failed_info = SourceCodeAnalyzer.extract_from_exception(@exception) || {}
|
40
|
+
end
|
41
|
+
|
29
42
|
if failed_info.empty?
|
30
|
-
|
43
|
+
report_data[:triage_result] = :unidentified_locator_issue
|
31
44
|
else
|
32
45
|
page_analyzer = PageAnalyzer.new(page_source, report_data[:platform].to_s)
|
33
|
-
all_page_elements = page_analyzer.analyze
|
34
|
-
|
46
|
+
all_page_elements = page_analyzer.analyze || []
|
47
|
+
similar_elements = Analyzer.find_similar_elements(failed_info, all_page_elements) || []
|
48
|
+
|
49
|
+
alternative_xpaths = []
|
50
|
+
if !similar_elements.empty?
|
51
|
+
target_suggestion = similar_elements.first
|
52
|
+
if target_suggestion[:attributes] && (target_path = target_suggestion[:attributes][:path])
|
53
|
+
target_node = doc.at_xpath(target_path)
|
54
|
+
alternative_xpaths = XPathFactory.generate_for_node(target_node) if target_node
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
unified_element_map = ElementRepository.load_all
|
59
|
+
de_para_result = Analyzer.find_de_para_match(failed_info, unified_element_map)
|
60
|
+
code_search_results = CodeSearcher.find_similar_locators(failed_info) || []
|
35
61
|
|
36
62
|
report_data.merge!({
|
37
63
|
page_source: page_source,
|
38
64
|
failed_element: failed_info,
|
39
|
-
|
65
|
+
similar_elements: similar_elements,
|
66
|
+
alternative_xpaths: alternative_xpaths,
|
67
|
+
de_para_analysis: de_para_result,
|
68
|
+
code_search_results: code_search_results,
|
40
69
|
all_page_elements: all_page_elements
|
41
70
|
})
|
42
71
|
end
|
@@ -44,8 +73,9 @@ module AppiumFailureHelper
|
|
44
73
|
|
45
74
|
ReportGenerator.new(@output_folder, report_data).generate_all
|
46
75
|
Utils.logger.info("Relatórios gerados com sucesso em: #{@output_folder}")
|
76
|
+
|
47
77
|
rescue => e
|
48
|
-
Utils.logger.error("Erro fatal na GEM: #{e.message}\n#{e.backtrace.join("\n")}")
|
78
|
+
Utils.logger.error("Erro fatal na GEM de diagnóstico: #{e.message}\n#{e.backtrace.join("\n")}")
|
49
79
|
end
|
50
80
|
end
|
51
81
|
end
|
@@ -18,72 +18,31 @@ module AppiumFailureHelper
|
|
18
18
|
File.write("#{@output_folder}/page_source_#{@data[:timestamp]}.xml", @page_source)
|
19
19
|
end
|
20
20
|
|
21
|
-
|
22
|
-
# Gera um YAML simplificado se não for um problema de seletor
|
21
|
+
def generate_yaml_reports
|
23
22
|
analysis_report = {
|
24
23
|
triage_result: @data[:triage_result],
|
25
24
|
exception_class: @data[:exception].class.to_s,
|
26
25
|
exception_message: @data[:exception].message,
|
27
26
|
failed_element: @data[:failed_element],
|
28
|
-
|
29
|
-
de_para_analysis: @data[:de_para_analysis],
|
30
|
-
code_search_results: @data[:code_search_results]
|
27
|
+
best_candidate_analysis: @data[:best_candidate_analysis]
|
31
28
|
}
|
32
29
|
File.open("#{@output_folder}/failure_analysis_#{@data[:timestamp]}.yaml", 'w') { |f| f.write(YAML.dump(analysis_report)) }
|
33
30
|
|
34
|
-
# Só gera o dump de elementos se a análise completa tiver sido feita
|
35
31
|
if @data[:all_page_elements]
|
36
32
|
File.open("#{@output_folder}/all_elements_dump_#{@data[:timestamp]}.yaml", 'w') { |f| f.write(YAML.dump(@data[:all_page_elements])) }
|
37
33
|
end
|
38
34
|
end
|
39
35
|
|
40
36
|
def generate_html_report
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
when :unidentified_locator_issue, :unidentified_timeout_issue
|
51
|
-
build_simple_diagnosis_report(
|
52
|
-
title: "Seletor Não Identificado",
|
53
|
-
message: "A falha ocorreu porque um elemento não foi encontrado, mas a GEM não conseguiu extrair o seletor exato da mensagem de erro ou do código-fonte. Isso geralmente acontece quando o seletor é construído dinamicamente ou está dentro de um método helper complexo. Verifique o stack trace para encontrar o método responsável (ex: 'tap_by_text')."
|
54
|
-
)
|
55
|
-
when :assertion_failure
|
56
|
-
build_simple_diagnosis_report(
|
57
|
-
title: "Falha de Asserção (Bug Funcional)",
|
58
|
-
message: "A automação executou os passos corretamente, mas o resultado final verificado na tela não foi o esperado. Isso geralmente indica um bug funcional na aplicação, e não um problema com o seletor."
|
59
|
-
)
|
60
|
-
when :visibility_issue
|
61
|
-
build_simple_diagnosis_report(
|
62
|
-
title: "Elemento Oculto ou Não-Interagível",
|
63
|
-
message: "O seletor encontrou o elemento no XML da página, mas ele não está visível ou habilitado para interação. Verifique se há outros elementos sobrepondo-o, se ele está desabilitado (disabled/enabled='false'), ou se é necessário aguardar uma animação."
|
64
|
-
)
|
65
|
-
when :stale_element_issue
|
66
|
-
build_simple_diagnosis_report(
|
67
|
-
title: "Referência de Elemento Antiga (Stale)",
|
68
|
-
message: "O elemento foi encontrado, mas a página foi atualizada antes que a interação pudesse ocorrer. Isso é um problema de timing. A solução é encontrar o elemento novamente logo antes de interagir com ele."
|
69
|
-
)
|
70
|
-
when :session_startup_issue
|
71
|
-
build_simple_diagnosis_report(
|
72
|
-
title: "Falha na Conexão com o Servidor Appium",
|
73
|
-
message: "Não foi possível criar uma sessão com o servidor. Verifique se o servidor Appium está rodando, se as 'capabilities' (incluindo prefixos 'appium:') e a URL de conexão estão corretas."
|
74
|
-
)
|
75
|
-
when :app_crash_issue
|
76
|
-
build_simple_diagnosis_report(
|
77
|
-
title: "Crash do Aplicativo",
|
78
|
-
message: "A sessão foi encerrada inesperadamente, o que indica que o aplicativo travou. A causa raiz deve ser investigada nos logs do dispositivo (Logcat para Android, Console para iOS)."
|
79
|
-
)
|
80
|
-
else # :ruby_code_issue, :unknown_issue
|
81
|
-
build_simple_diagnosis_report(
|
82
|
-
title: "Erro Inesperado",
|
83
|
-
message: "Ocorreu um erro não catalogado. Verifique o stack trace para mais detalhes."
|
84
|
-
)
|
85
|
-
end
|
86
|
-
end
|
37
|
+
html_content = case @data[:triage_result]
|
38
|
+
when :locator_issue
|
39
|
+
build_full_report
|
40
|
+
else
|
41
|
+
build_simple_diagnosis_report(
|
42
|
+
title: "Diagnóstico Rápido de Falha",
|
43
|
+
message: "A falha não foi causada por um seletor não encontrado ou a análise do seletor falhou. Verifique a mensagem de erro original e o stack trace para a causa raiz."
|
44
|
+
)
|
45
|
+
end
|
87
46
|
|
88
47
|
File.write("#{@output_folder}/report_#{@data[:timestamp]}.html", html_content)
|
89
48
|
end
|
@@ -1,28 +1,20 @@
|
|
1
1
|
# lib/appium_failure_helper/source_code_analyzer.rb
|
2
2
|
module AppiumFailureHelper
|
3
3
|
module SourceCodeAnalyzer
|
4
|
-
# VERSÃO 3.0: Padrões de Regex mais flexíveis que aceitam um "receptor" opcional (como $driver).
|
5
4
|
PATTERNS = [
|
6
5
|
{ type: 'id', regex: /(?:\$driver\.)?find_element\((?:id:|:id\s*=>)\s*['"]([^'"]+)['"]\)/ },
|
7
6
|
{ type: 'xpath', regex: /(?:\$driver\.)?find_element\((?:xpath:|:xpath\s*=>)\s*['"]([^'"]+)['"]\)/ },
|
8
|
-
{ type: 'accessibility_id', regex: /(?:\$driver\.)?find_element\((?:accessibility_id:|:accessibility_id\s*=>)\s*['"]([^'"]+)['"]\)/ },
|
9
|
-
{ type: 'class_name', regex: /(?:\$driver\.)?find_element\((?:class_name:|:class_name\s*=>)\s*['"]([^'"]+)['"]\)/ },
|
10
|
-
{ type: 'xpath', regex: /(?:\$driver\.)?find_element\(:xpath,\s*['"]([^'"]+)['"]\)/ },
|
11
7
|
{ type: 'id', regex: /(?:\$driver\.)?\s*id\s*\(?['"]([^'"]+)['"]\)?/ },
|
12
8
|
{ type: 'xpath', regex: /(?:\$driver\.)?\s*xpath\s*\(?['"]([^'"]+)['"]\)?/ }
|
13
9
|
].freeze
|
14
10
|
|
15
11
|
def self.extract_from_exception(exception)
|
16
|
-
# Busca a primeira linha do backtrace que seja um arquivo .rb do projeto
|
17
12
|
location = exception.backtrace.find { |line| line.include?('.rb') && !line.include?('gems') }
|
18
13
|
return {} unless location
|
19
|
-
|
20
14
|
path_match = location.match(/^(.*?):(\d+)(?::in.*)?$/)
|
21
15
|
return {} unless path_match
|
22
|
-
|
23
16
|
file_path, line_number = path_match.captures
|
24
17
|
return {} unless File.exist?(file_path)
|
25
|
-
|
26
18
|
begin
|
27
19
|
error_line = File.readlines(file_path)[line_number.to_i - 1]
|
28
20
|
return parse_line_for_locator(error_line)
|