From 5ef35813eb1c66b5e9231f70f1dfb8a2c89e0642 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Sun, 13 Oct 2024 11:10:48 +0200 Subject: [PATCH] feat: Switch the XML-related stuff to use NSXMLDocument (#314) --- .../IntegrationTests/AMFindElementTests.m | 10 + .../WebDriverAgentLib/Utilities/FBXPath.m | 252 +++++------------- .../project.pbxproj | 2 - 3 files changed, 75 insertions(+), 189 deletions(-) diff --git a/WebDriverAgentMac/IntegrationTests/AMFindElementTests.m b/WebDriverAgentMac/IntegrationTests/AMFindElementTests.m index 833c76c..25e6961 100644 --- a/WebDriverAgentMac/IntegrationTests/AMFindElementTests.m +++ b/WebDriverAgentMac/IntegrationTests/AMFindElementTests.m @@ -95,6 +95,16 @@ - (void)testMultipleDescendantsWithXPath XCTAssertEqualObjects([matches objectAtIndex:2].identifier, @"_XCUI:MinimizeWindow"); } +- (void)testMultipleDescendantsWithXPath2 +{ + NSString *query = @"*//XCUIElementTypeButton[matches(@identifier, \"_xcui:\", \"i\")]"; + NSArray *matches = [self.testedApplication fb_descendantsMatchingXPathQuery:query + shouldReturnAfterFirstMatch:NO]; + XCTAssertTrue(matches.count >= 3); + XCTAssertEqualObjects(matches.firstObject.identifier, @"_XCUI:CloseWindow"); + XCTAssertEqualObjects([matches objectAtIndex:2].identifier, @"_XCUI:MinimizeWindow"); +} + - (void)testSingleDescendantWithClassChain { NSString *query = @"**/XCUIElementTypeButton[`identifier == '_XCUI:CloseWindow'`]"; diff --git a/WebDriverAgentMac/WebDriverAgentLib/Utilities/FBXPath.m b/WebDriverAgentMac/WebDriverAgentLib/Utilities/FBXPath.m index 984df31..8928e9e 100644 --- a/WebDriverAgentMac/WebDriverAgentLib/Utilities/FBXPath.m +++ b/WebDriverAgentMac/WebDriverAgentLib/Utilities/FBXPath.m @@ -19,22 +19,6 @@ #import "NSString+FBXMLSafeString.h" #import "XCUIElementQuery+AMHelpers.h" -#ifdef __clang__ -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wpadded" -#endif - -#import -#import -#import -#import -#import -#import - -#ifdef __clang__ -#pragma clang diagnostic pop -#endif - @interface FBElementAttribute : NSObject @@ -43,7 +27,7 @@ @interface FBElementAttribute : NSObject + (nonnull NSString *)name; + (nullable NSString *)valueForElement:(id)element; -+ (int)recordWithWriter:(xmlTextWriterPtr)writer forElement:(id)element; ++ (void)recordWithNode:(NSXMLElement *)node forElement:(id)element; + (NSArray *)supportedAttributes; @@ -105,12 +89,10 @@ @interface FBInternalIndexAttribute : FBElementAttribute @property (nonatomic, nonnull, readonly) NSString* indexValue; -+ (int)recordWithWriter:(xmlTextWriterPtr)writer forValue:(NSString *)value; ++ (void)recordWithNode:(NSXMLElement *)node forValue:(NSString *)value; @end -const static char *_UTF8Encoding = "UTF-8"; - static NSString *const kXMLIndexPathKey = @"private_indexPath"; @@ -131,25 +113,8 @@ + (nullable NSString *)xmlStringWithRootElement:(XCUIElement *)root [FBLogger logFmt:@"The snapshot of %@ cannot be taken. Original error: %@", root.description, error.description]; return nil; } - - xmlDocPtr doc; - xmlTextWriterPtr writer = xmlNewTextWriterDoc(&doc, 0); - int rc = [self xmlRepresentationWithRootElement:snapshot - writer:writer - query:nil]; - if (rc < 0) { - xmlFreeTextWriter(writer); - xmlFreeDoc(doc); - return nil; - } - int buffersize; - xmlChar *xmlbuff; - xmlDocDumpFormatMemory(doc, &xmlbuff, &buffersize, 1); - xmlFreeTextWriter(writer); - xmlFreeDoc(doc); - NSString *result = [NSString stringWithCString:(const char *)xmlbuff encoding:NSUTF8StringEncoding]; - xmlFree(xmlbuff); - return result; + + return [self xmlRepresentationWithRootElement:snapshot].XMLString; } + (NSArray *)matchesWithRootElement:(XCUIElement *)root @@ -165,64 +130,44 @@ + (nullable NSString *)xmlStringWithRootElement:(XCUIElement *)root userInfo:@{}]; } - xmlDocPtr doc; - xmlTextWriterPtr writer = xmlNewTextWriterDoc(&doc, 0); - if (NULL == writer) { - [FBLogger logFmt:@"Failed to invoke libxml2>xmlNewTextWriterDoc for XPath query \"%@\"", xpathQuery]; - return [self throwException:FBXPathQueryEvaluationException forQuery:xpathQuery]; - } - int rc = [self xmlRepresentationWithRootElement:snapshot - writer:writer - query:xpathQuery]; - if (rc < 0) { - xmlFreeTextWriter(writer); - xmlFreeDoc(doc); - return [self throwException:FBXPathQueryEvaluationException forQuery:xpathQuery]; - } - - xmlXPathObjectPtr queryResult = [self evaluate:xpathQuery document:doc]; - if (NULL == queryResult) { - xmlFreeTextWriter(writer); - xmlFreeDoc(doc); - return [self throwException:FBInvalidXPathException forQuery:xpathQuery]; + NSXMLElement *rootElement = [self writeXmlWithRootSnapshot:snapshot + indexPath:[AMSnapshotUtils hashWithSnapshot:snapshot]]; + NSArray<__kindof NSXMLNode *> *matches = [rootElement nodesForXPath:xpathQuery error:&error]; + if (nil == matches) { + @throw [NSException exceptionWithName:FBInvalidXPathException + reason:error.description + userInfo:@{}]; } - NSArray *matchingElements = [self collectMatchingElementsWithNodeSet:queryResult->nodesetval - rootElement:root - rootSnapshot:snapshot - includeOnlyFirstMatch:firstMatch]; - xmlXPathFreeObject(queryResult); - xmlFreeTextWriter(writer); - xmlFreeDoc(doc); + NSArray *matchingElements = [self collectMatchingElementsWithNodes:matches + rootElement:root + rootSnapshot:snapshot + includeOnlyFirstMatch:firstMatch]; if (nil == matchingElements) { return [self throwException:FBXPathQueryEvaluationException forQuery:xpathQuery]; } return matchingElements; } -+ (NSArray *)collectMatchingElementsWithNodeSet:(xmlNodeSetPtr)nodeSet - rootElement:(XCUIElement *)rootElement - rootSnapshot:(id)rootSnapshot - includeOnlyFirstMatch:(BOOL)firstMatch ++ (NSArray *)collectMatchingElementsWithNodes:(NSArray<__kindof NSXMLNode *> *)nodes + rootElement:(XCUIElement *)rootElement + rootSnapshot:(id)rootSnapshot + includeOnlyFirstMatch:(BOOL)firstMatch { - if (xmlXPathNodeSetIsEmpty(nodeSet)) { + if (0 == nodes.count) { return @[]; } - const xmlChar *indexPathKeyName = (xmlChar *)[kXMLIndexPathKey UTF8String]; NSMutableArray *hashes = [NSMutableArray array]; - for (NSInteger i = 0; i < nodeSet->nodeNr; i++) { - xmlNodePtr currentNode = nodeSet->nodeTab[i]; - xmlChar *attrValue = xmlGetProp(currentNode, indexPathKeyName); - if (NULL == attrValue) { - [FBLogger log:@"Failed to invoke libxml2>xmlGetProp"]; - return nil; + for (NSXMLNode *node in nodes) { + if (![node isKindOfClass:NSXMLElement.class]) { + continue; } - - NSString *hash = [NSString stringWithCString:(const char *)attrValue - encoding:NSUTF8StringEncoding]; - [hashes addObject:hash]; - xmlFree(attrValue); + NSString *attrValue = [[(NSXMLElement *)node attributeForName:kXMLIndexPathKey] stringValue]; + if (nil == attrValue) { + continue; + } + [hashes addObject:attrValue]; } NSMutableArray *matchingElements = [NSMutableArray array]; NSString *selfHash = [AMSnapshotUtils hashWithSnapshot:rootSnapshot]; @@ -241,49 +186,13 @@ + (NSArray *)collectMatchingElementsWithNodeSet:(xmlNodeSetPtr)nodeSet : matchingElements.copy; } -+ (int)xmlRepresentationWithRootElement:(id)root - writer:(xmlTextWriterPtr)writer - query:(nullable NSString*)query ++ (NSXMLDocument *)xmlRepresentationWithRootElement:(id)root { - int rc = xmlTextWriterStartDocument(writer, NULL, _UTF8Encoding, NULL); - if (rc < 0) { - [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartDocument. Error code: %d", rc]; - return rc; - } - - NSString *index = [AMSnapshotUtils hashWithSnapshot:root]; - rc = [self writeXmlWithRootElement:root - indexPath:(query != nil ? index : nil) - writer:writer]; - if (rc < 0) { - [FBLogger log:@"Failed to generate XML presentation of a screen element"]; - return rc; - } - rc = xmlTextWriterEndDocument(writer); - if (rc < 0) { - [FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathNewContext. Error code: %d", rc]; - return rc; - } - return 0; -} - -+ (xmlXPathObjectPtr)evaluate:(NSString *)xpathQuery document:(xmlDocPtr)doc -{ - xmlXPathContextPtr xpathCtx = xmlXPathNewContext(doc); - if (NULL == xpathCtx) { - [FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathNewContext for XPath query \"%@\"", xpathQuery]; - return NULL; - } - xpathCtx->node = doc->children; - - xmlXPathObjectPtr xpathObj = xmlXPathEvalExpression((xmlChar *)[xpathQuery UTF8String], xpathCtx); - if (NULL == xpathObj) { - xmlXPathFreeContext(xpathCtx); - [FBLogger logFmt:@"Failed to invoke libxml2>xmlXPathEvalExpression for XPath query \"%@\"", xpathQuery]; - return NULL; - } - xmlXPathFreeContext(xpathCtx); - return xpathObj; + NSXMLElement *rootElement = [self writeXmlWithRootSnapshot:root indexPath:nil]; + NSXMLDocument *xmlDoc = [[NSXMLDocument alloc] initWithRootElement:rootElement]; + [xmlDoc setVersion:@"1.0"]; + [xmlDoc setCharacterEncoding:@"UTF-8"]; + return xmlDoc; } + (nullable NSString *)safeXmlStringWithString:(nullable NSString *)str @@ -291,64 +200,39 @@ + (nullable NSString *)safeXmlStringWithString:(nullable NSString *)str return [str fb_xmlSafeStringWithReplacement:@""]; } -+ (int)recordElementAttributes:(xmlTextWriterPtr)writer - forElement:(id)element - indexPath:(nullable NSString *)indexPath ++ (void)recordElementAttributes:(NSXMLElement *)node + forSnapshot:(id)snapshot + indexPath:(nullable NSString *)indexPath { for (Class attributeCls in FBElementAttribute.supportedAttributes) { - int rc = [attributeCls recordWithWriter:writer forElement:element]; - if (rc < 0) { - return rc; - } + [attributeCls recordWithNode:node forElement:snapshot]; } if (nil != indexPath) { // index path is the special case - return [FBInternalIndexAttribute recordWithWriter:writer forValue:indexPath]; + [FBInternalIndexAttribute recordWithNode:node forValue:indexPath]; } - return 0; } -+ (int)writeXmlWithRootElement:(id)root - indexPath:(nullable NSString *)indexPath - writer:(xmlTextWriterPtr)writer ++ (NSXMLElement *)writeXmlWithRootSnapshot:(id)root + indexPath:(nullable NSString *)indexPath { - id currentSnapshot = root; - NSArray> *children = root.children; + NSString *type = [FBElementTypeTransformer stringWithElementType:root.elementType]; + NSXMLElement *rootElement = [NSXMLElement elementWithName:type]; + [self recordElementAttributes:rootElement + forSnapshot:root + indexPath:indexPath]; - NSString *type = [FBElementTypeTransformer stringWithElementType:currentSnapshot.elementType]; - int rc = xmlTextWriterStartElement(writer, (xmlChar *)[type UTF8String]); - if (rc < 0) { - [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterStartElement for the tag value '%@'. Error code: %d", type, rc]; - return rc; - } - - rc = [self recordElementAttributes:writer - forElement:currentSnapshot - indexPath:indexPath]; - if (rc < 0) { - return rc; - } - - for (NSUInteger i = 0; i < [children count]; i++) { - id childSnapshot = [children objectAtIndex:i]; + NSArray> *children = root.children; + for (id childSnapshot in children) { NSString *newIndexPath = (indexPath != nil) ? [AMSnapshotUtils hashWithSnapshot:childSnapshot] : nil; - rc = [self writeXmlWithRootElement:childSnapshot - indexPath:newIndexPath - writer:writer]; - if (rc < 0) { - return rc; - } - } - - rc = xmlTextWriterEndElement(writer); - if (rc < 0) { - [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterEndElement. Error code: %d", rc]; - return rc; + NSXMLElement *childElement = [self writeXmlWithRootSnapshot:childSnapshot + indexPath:newIndexPath]; + [rootElement addChild:childElement]; } - return 0; + return rootElement; } @end @@ -379,20 +263,17 @@ + (NSString *)valueForElement:(id)element @throw [NSException exceptionWithName:FBAbstractMethodInvocationException reason:errMsg userInfo:nil]; } -+ (int)recordWithWriter:(xmlTextWriterPtr)writer forElement:(id)element ++ (void)recordWithNode:(NSXMLElement *)node forElement:(id)element { NSString *value = [self valueForElement:element]; if (nil == value) { // Skip the attribute if the value equals to nil - return 0; + return; } - int rc = xmlTextWriterWriteAttribute(writer, - (xmlChar *)[[FBXPath safeXmlStringWithString:self.name] UTF8String], - (xmlChar *)[[FBXPath safeXmlStringWithString:value] UTF8String]); - if (rc < 0) { - [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterWriteAttribute(%@='%@'). Error code: %d", self.name, value, rc]; - } - return rc; + + NSString *attrName = [FBXPath safeXmlStringWithString:self.name]; + NSString *attrValue = [FBXPath safeXmlStringWithString:value]; + [node addAttribute:[NSXMLNode attributeWithName:attrName stringValue:attrValue]]; } + (NSArray *)supportedAttributes @@ -579,26 +460,23 @@ + (NSString *)valueForElement:(id)element @end -@implementation FBInternalIndexAttribute +@implementation FBInternalIndexAttribute: FBElementAttribute + (NSString *)name { return kXMLIndexPathKey; } -+ (int)recordWithWriter:(xmlTextWriterPtr)writer forValue:(NSString *)value ++ (void)recordWithNode:(NSXMLElement *)node forValue:(NSString *)value { if (nil == value) { // Skip the attribute if the value equals to nil - return 0; + return; } - int rc = xmlTextWriterWriteAttribute(writer, - (xmlChar *)[[FBXPath safeXmlStringWithString:[self name]] UTF8String], - (xmlChar *)[[FBXPath safeXmlStringWithString:value] UTF8String]); - if (rc < 0) { - [FBLogger logFmt:@"Failed to invoke libxml2>xmlTextWriterWriteAttribute(%@='%@'). Error code: %d", [self name], value, rc]; - } - return rc; + + NSString *attrName = [FBXPath safeXmlStringWithString:self.name]; + NSString *attrValue = [FBXPath safeXmlStringWithString:value]; + [node addAttribute:[NSXMLNode attributeWithName:attrName stringValue:attrValue]]; } @end diff --git a/WebDriverAgentMac/WebDriverAgentMac.xcodeproj/project.pbxproj b/WebDriverAgentMac/WebDriverAgentMac.xcodeproj/project.pbxproj index 7091436..8dbed0f 100644 --- a/WebDriverAgentMac/WebDriverAgentMac.xcodeproj/project.pbxproj +++ b/WebDriverAgentMac/WebDriverAgentMac.xcodeproj/project.pbxproj @@ -125,7 +125,6 @@ 714CA7022566475200353B27 /* XCUIApplication+AMSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 714CA7002566475200353B27 /* XCUIApplication+AMSource.m */; }; 714CA7062566487B00353B27 /* FBXPath.h in Headers */ = {isa = PBXBuildFile; fileRef = 714CA7042566487B00353B27 /* FBXPath.h */; }; 714CA7072566487B00353B27 /* FBXPath.m in Sources */ = {isa = PBXBuildFile; fileRef = 714CA7052566487B00353B27 /* FBXPath.m */; }; - 714CA70A256648B200353B27 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 714CA709256648A100353B27 /* libxml2.tbd */; }; 71688A98256461ED0007F55B /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 71688A97256461ED0007F55B /* AppDelegate.m */; }; 71688A9B256461ED0007F55B /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 71688A9A256461ED0007F55B /* ViewController.m */; }; 71688A9D256461F00007F55B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 71688A9C256461F00007F55B /* Assets.xcassets */; }; @@ -455,7 +454,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 714CA70A256648B200353B27 /* libxml2.tbd in Frameworks */, 7199B3CD2565B1CD000B5C51 /* XCTest.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0;