Skip to content

Commit f17be99

Browse files
authored
[SDTEST-3546] Test tags for the advanced features (#229)
* WIP. Added tags for ST. Updated test runners. * added non-retriable tag. allowed overrides. tests * small fix
1 parent 3e72e81 commit f17be99

27 files changed

Lines changed: 529 additions & 220 deletions

DatadogSDKTesting.xcodeproj/project.pbxproj

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@
4949
A74EB8E62D0B449300FAD9B8 /* TestCodeCoverage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A74EB8E52D0B449300FAD9B8 /* TestCodeCoverage.swift */; };
5050
A754BB0A2F86CD3B001613EB /* MockSwiftTesting.swift in Sources */ = {isa = PBXBuildFile; fileRef = A754BB092F86CD34001613EB /* MockSwiftTesting.swift */; };
5151
A754BB0C2F8958A6001613EB /* TestManagementSwiftTestingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A754BB0B2F8958A6001613EB /* TestManagementSwiftTestingTests.swift */; };
52+
A754BB0E2F8E6E9E001613EB /* SwiftTestingTags.swift in Sources */ = {isa = PBXBuildFile; fileRef = A754BB0D2F8E6E91001613EB /* SwiftTestingTags.swift */; };
53+
A754BB102F8E7930001613EB /* XCTestTags.swift in Sources */ = {isa = PBXBuildFile; fileRef = A754BB0F2F8E792C001613EB /* XCTestTags.swift */; };
54+
A754BB122F8E79AA001613EB /* TestTags.swift in Sources */ = {isa = PBXBuildFile; fileRef = A754BB112F8E79A4001613EB /* TestTags.swift */; };
5255
A75D33832DC28E8900553EDA /* MockTestTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = A75D33822DC28E8100553EDA /* MockTestTypes.swift */; };
5356
A75DAB9C2DEE18DD00F1B8A7 /* MockTestRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = A75DAB9B2DEE18D600F1B8A7 /* MockTestRunner.swift */; };
5457
A76174BD2CB3E3780098CE99 /* DDXCTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = A76174BC2CB3E3780098CE99 /* DDXCTestCase.swift */; };
@@ -64,9 +67,6 @@
6467
A7675D722F6CA00000A05B5F /* SwiftTestRunParametersParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7675D712F6CA00000A05B5F /* SwiftTestRunParametersParserTests.swift */; };
6568
A7675D742F6CA00000A05B5F /* SessionManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7675D732F6CA00000A05B5F /* SessionManagerTests.swift */; };
6669
A76F0F992E2A9F0D009ACD06 /* TestImpactAnalysisTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A76F0F982E2A9F02009ACD06 /* TestImpactAnalysisTests.swift */; };
67-
E5BF2275FF5E4C7D9EE74AD2 /* TestImpactAnalysisSwiftTestingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9081E25D194D74A4DA020E /* TestImpactAnalysisSwiftTestingTests.swift */; };
68-
A7DEAD022F3DEAD00000001A /* ParallelTestRunnerDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7DEAD012F3DEAD00000001A /* ParallelTestRunnerDetector.swift */; };
69-
A7DEAD042F3DEAD00000001A /* ParallelTestRunnerDetectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7DEAD032F3DEAD00000001A /* ParallelTestRunnerDetectorTests.swift */; };
7070
A77C122F2D89CFC2009C2BA1 /* SDKCapabilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = A77C122E2D89CFC2009C2BA1 /* SDKCapabilities.swift */; };
7171
A77C12312D8C976B009C2BA1 /* TestManagementService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A77C12302D8C976B009C2BA1 /* TestManagementService.swift */; };
7272
A77C12332D8C9798009C2BA1 /* KnownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A77C12322D8C9798009C2BA1 /* KnownTests.swift */; };
@@ -394,6 +394,9 @@
394394
A74EB8E52D0B449300FAD9B8 /* TestCodeCoverage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestCodeCoverage.swift; sourceTree = "<group>"; };
395395
A754BB092F86CD34001613EB /* MockSwiftTesting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSwiftTesting.swift; sourceTree = "<group>"; };
396396
A754BB0B2F8958A6001613EB /* TestManagementSwiftTestingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestManagementSwiftTestingTests.swift; sourceTree = "<group>"; };
397+
A754BB0D2F8E6E91001613EB /* SwiftTestingTags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftTestingTags.swift; sourceTree = "<group>"; };
398+
A754BB0F2F8E792C001613EB /* XCTestTags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTestTags.swift; sourceTree = "<group>"; };
399+
A754BB112F8E79A4001613EB /* TestTags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTags.swift; sourceTree = "<group>"; };
397400
A75D33822DC28E8100553EDA /* MockTestTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTestTypes.swift; sourceTree = "<group>"; };
398401
A75DAB9B2DEE18D600F1B8A7 /* MockTestRunner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockTestRunner.swift; sourceTree = "<group>"; };
399402
A76174BC2CB3E3780098CE99 /* DDXCTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDXCTestCase.swift; sourceTree = "<group>"; };
@@ -408,9 +411,6 @@
408411
A7675D712F6CA00000A05B5F /* SwiftTestRunParametersParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftTestRunParametersParserTests.swift; sourceTree = "<group>"; };
409412
A7675D732F6CA00000A05B5F /* SessionManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionManagerTests.swift; sourceTree = "<group>"; };
410413
A76F0F982E2A9F02009ACD06 /* TestImpactAnalysisTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestImpactAnalysisTests.swift; sourceTree = "<group>"; };
411-
0A9081E25D194D74A4DA020E /* TestImpactAnalysisSwiftTestingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestImpactAnalysisSwiftTestingTests.swift; sourceTree = "<group>"; };
412-
A7DEAD012F3DEAD00000001A /* ParallelTestRunnerDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallelTestRunnerDetector.swift; sourceTree = "<group>"; };
413-
A7DEAD032F3DEAD00000001A /* ParallelTestRunnerDetectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallelTestRunnerDetectorTests.swift; sourceTree = "<group>"; };
414414
A77C122E2D89CFC2009C2BA1 /* SDKCapabilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKCapabilities.swift; sourceTree = "<group>"; };
415415
A77C12302D8C976B009C2BA1 /* TestManagementService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestManagementService.swift; sourceTree = "<group>"; };
416416
A77C12322D8C9798009C2BA1 /* KnownTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KnownTests.swift; sourceTree = "<group>"; };
@@ -733,6 +733,7 @@
733733
A7675D462F631AF000A05B5F /* XCTest */ = {
734734
isa = PBXGroup;
735735
children = (
736+
A754BB0F2F8E792C001613EB /* XCTestTags.swift */,
736737
A76174BC2CB3E3780098CE99 /* DDXCTestCase.swift */,
737738
E1ABA07025231F4700ED8AEF /* DDXCTestObserver.swift */,
738739
A76174BE2CB45F9B0098CE99 /* XCTestExtensions.swift */,
@@ -749,6 +750,7 @@
749750
A7675D4E2F6980D000A05B5F /* SwiftTestingObserver.swift */,
750751
A7675D4A2F64742500A05B5F /* SwiftTestingContext.swift */,
751752
A7675D482F631DDB00A05B5F /* SwiftTestingTrait.swift */,
753+
A754BB0D2F8E6E91001613EB /* SwiftTestingTags.swift */,
752754
);
753755
path = SwiftTesting;
754756
sourceTree = "<group>";
@@ -1138,6 +1140,7 @@
11381140
A7675D6B2F6C58BB00A05B5F /* SessionManager.swift */,
11391141
A79FAD982F8548700080C3F6 /* SessionAndModuleObserver.swift */,
11401142
A77C12342D8C9F5D009C2BA1 /* TestManagement.swift */,
1143+
A754BB112F8E79A4001613EB /* TestTags.swift */,
11411144
A79FAD9A2F8559530080C3F6 /* AdditionalTagsFeature.swift */,
11421145
E158E2DB25471DA000DBCD6C /* DDInstrumentationControl.swift */,
11431146
8194A2242940B33100B4B592 /* DDTestSession.swift */,
@@ -1692,6 +1695,7 @@
16921695
A7FC25BB2BA1EADF00067E26 /* DDTestMonitor.swift in Sources */,
16931696
A7FC260A2BA1EC0500067E26 /* Log.swift in Sources */,
16941697
A71E64712BAA0AA100F2ACA5 /* AppveyorCI.swift in Sources */,
1698+
A754BB0E2F8E6E9E001613EB /* SwiftTestingTags.swift in Sources */,
16951699
A7FC26142BA1EC0500067E26 /* XCUIApplicationSwizzler.swift in Sources */,
16961700
A7675D4B2F64742C00A05B5F /* SwiftTestingContext.swift in Sources */,
16971701
A71E64722BAA0AA100F2ACA5 /* BuildkiteCI.swift in Sources */,
@@ -1700,6 +1704,7 @@
17001704
A7FC25B52BA1EADF00067E26 /* DDInstrumentationControl.swift in Sources */,
17011705
A71E646E2BAA0AA100F2ACA5 /* GithubCI.swift in Sources */,
17021706
A7675D492F631DF400A05B5F /* SwiftTestingTrait.swift in Sources */,
1707+
A754BB122F8E79AA001613EB /* TestTags.swift in Sources */,
17031708
A7FC25B82BA1EADF00067E26 /* DDTracer.swift in Sources */,
17041709
A7FC26092BA1EC0500067E26 /* Clock.swift in Sources */,
17051710
A7FC260E2BA1EC0500067E26 /* PlatformUtils.swift in Sources */,
@@ -1738,6 +1743,7 @@
17381743
A7FC25B62BA1EADF00067E26 /* DDTestModule.swift in Sources */,
17391744
A7B012132F364C04000270A4 /* URLSessionInstrumentationConfiguration.swift in Sources */,
17401745
A71E646A2BAA0AA100F2ACA5 /* Environment.swift in Sources */,
1746+
A754BB102F8E7930001613EB /* XCTestTags.swift in Sources */,
17411747
A7FC25D22BA1EB2900067E26 /* SimpleSpanSerializer.swift in Sources */,
17421748
A7FC25BA2BA1EADF00067E26 /* DDTags.swift in Sources */,
17431749
A7675D4F2F6980D600A05B5F /* SwiftTestingObserver.swift in Sources */,

Sources/DatadogSDKTesting/AutomaticTestRetries.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ final class AutomaticTestRetries: TestHooksFeature {
3636
withStatus status: TestStatus, retryStatus: RetryStatus.Iterator,
3737
andInfo info: TestRunInfoStart) -> RetryStatus.Iterator
3838
{
39+
guard info.tags.get(tag: .retriable) ?? true else { return retryStatus.next() }
3940
if case .fail = status {
4041
if info.executions.total < failedTestRetriesCount // we can retry more
4142
&& incrementRetries() != nil // and increased global retry counter successfully
@@ -52,6 +53,7 @@ final class AutomaticTestRetries: TestHooksFeature {
5253
}
5354

5455
func shouldSuppressError(test: any TestRun, info: TestRunInfoStart) -> Bool {
56+
guard info.tags.get(tag: .retriable) ?? true else { return false }
5557
return info.executions.total < failedTestRetriesCount // we can retry test more
5658
&& _failedTestTotalRetries.value < failedTestTotalRetriesMax // and global counter allow us to retry
5759
}

Sources/DatadogSDKTesting/EarlyFlakeDetection.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ final class EarlyFlakeDetection: TestHooksFeature {
106106
withStatus: TestStatus, retryStatus: RetryStatus.Iterator,
107107
andInfo info: TestRunInfoStart) -> RetryStatus.Iterator
108108
{
109+
guard info.tags.get(tag: .retriable) ?? true else { return retryStatus.next() }
109110
// EFD should be enabled for this test
110111
guard checkStatus(for: test) else { return retryStatus.next() }
111112

@@ -132,6 +133,7 @@ final class EarlyFlakeDetection: TestHooksFeature {
132133
}
133134

134135
func shouldSuppressError(test: any TestRun, info: TestRunInfoStart) -> Bool {
136+
guard info.tags.get(tag: .retriable) ?? true else { return false }
135137
// if EFD enabled for this test suppress the error for now. We will handle it after in testGroupRetry
136138
return checkStatus(for: test)
137139
}

Sources/DatadogSDKTesting/Feature.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,8 @@ extension RetryStatus {
361361
}
362362

363363

364-
struct TestRunInfo<RetryInfo> {
364+
struct TestRunInfo<RetryInfo: Sendable>: Sendable {
365+
let tags: any TestTags
365366
var skip: (by: (feature: FeatureId, reason: String)?, status: SkipStatus)
366367
var retry: RetryInfo
367368
var executions: (total: Int, failed: Int)
@@ -377,7 +378,7 @@ extension TestRunInfoEnd {
377378
}.map {
378379
(feature: $0, reason: $1, errors: retry.status.errorsStatus)
379380
}
380-
return .init(skip: skip, retry: retryStart, executions: executions)
381+
return .init(tags: tags, skip: skip, retry: retryStart, executions: executions)
381382
}
382383
}
383384

Sources/DatadogSDKTesting/Models.swift

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -149,14 +149,6 @@ extension TestRun {
149149
}
150150
}
151151

152-
protocol TestTag {
153-
associatedtype Value
154-
}
155-
156-
protocol TestTags {
157-
func get<T: TestTag>(tag: T) -> T.Value?
158-
}
159-
160152
@objc(DDTestStatus)
161153
public enum TestStatus: Int, Sendable {
162154
case pass

Sources/DatadogSDKTesting/SwiftTesting/SwiftTestingContext.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ protocol SwiftTestingTestInfoType: Sendable {
1414
var hasSuite: Bool { get }
1515
var suite: String { get }
1616
var isParameterized: Bool { get }
17+
var attachedTags: any TestTags { get }
1718
}
1819

1920
struct SwiftTestingSourceLocation {
@@ -297,7 +298,8 @@ struct SwiftTestingRetryGroupContext: Sendable {
297298
skipStrategy: RetryGroupSkipStrategy, successStrategy: RetryGroupSuccessStrategy)
298299
{
299300
self.test = test
300-
self.info = .init(skip: skip,
301+
self.info = .init(tags: test.attachedTags,
302+
skip: skip,
301303
retry: (nil, .end(errors: .unsuppressed)),
302304
executions: (0, 0))
303305
self.executions = []
@@ -438,6 +440,8 @@ final class SwiftTestingTestContext: Sendable {
438440
suite.observer
439441
}
440442

443+
var attachedTags: any TestTags { info.attachedTags }
444+
441445
init(suite: SwiftTestingSuiteContext, info: any SwiftTestingTestInfoType) {
442446
self.suite = suite
443447
self.info = info

Sources/DatadogSDKTesting/SwiftTesting/SwiftTestingObserver.swift

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ struct SwiftTestingObserver: SwiftTestingObserverType {
5757
test: borrowing SwiftTestingTestContext
5858
) async -> (feature: FeatureId?, configuration: RetryGroupConfiguration) {
5959
let (feautre, config) = test.configuration.activeFeatures.testGroupConfiguration(for: test.info.name,
60-
tags: DummyTags(),
60+
tags: test.attachedTags,
6161
in: test.suite.suite)
6262
return (feautre?.id, config)
6363
}
@@ -104,11 +104,3 @@ struct SwiftTestingObserver: SwiftTestingObserverType {
104104
test.configuration.activeFeatures.testDidFinish(test: test.testRun, info: info)
105105
}
106106
}
107-
108-
extension SwiftTestingObserver {
109-
struct DummyTags: TestTags {
110-
func get<T: TestTag>(tag: T) -> T.Value? {
111-
return nil
112-
}
113-
}
114-
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3+
* This product includes software developed at Datadog (https://www.datadoghq.com/).
4+
* Copyright 2020-Present Datadog, Inc.
5+
*/
6+
7+
import Foundation
8+
9+
#if canImport(Testing)
10+
import Testing
11+
12+
extension Tag {
13+
enum dd {
14+
@Tag static var retriable: Tag
15+
@Tag static var nonretriable: Tag
16+
}
17+
}
18+
19+
protocol STTestTag: TestTag {
20+
func parse(tags: borrowing Set<Tag>) -> Value?
21+
}
22+
23+
struct STTestTags: TestTags {
24+
let tags: Set<Tag>
25+
26+
init(test: borrowing Testing.Test) {
27+
self.tags = test.tags
28+
}
29+
30+
func get<T: TestTag>(tag: T) -> T.Value? {
31+
guard let tag = tag as? any STTestTag else {
32+
return nil
33+
}
34+
// have to cast because normal types work from macOS 13 only
35+
return tag.parse(tags: tags) as! T.Value?
36+
}
37+
}
38+
39+
extension Testing.Test {
40+
var attachedTags: STTestTags { .init(test: self) }
41+
}
42+
43+
// Retriable tag
44+
extension RetriableTag: STTestTag {
45+
func parse(tags: borrowing Set<Tag>) -> Bool? {
46+
if tags.contains(.dd.retriable) {
47+
return true
48+
}
49+
if tags.contains(.dd.nonretriable) {
50+
return false
51+
}
52+
return nil
53+
}
54+
}
55+
#endif

Sources/DatadogSDKTesting/SwiftTesting/SwiftTestingTrait.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ extension DatadogSwiftTestingScopeProvider {
179179
var isSuite: Bool { test.isSuite }
180180
var hasSuite: Bool { test.ddHasSuite }
181181
var isParameterized: Bool { test.isParameterized }
182+
var attachedTags: any TestTags { test.attachedTags }
182183
}
183184

184185
struct SwiftTestRun: SwiftTestingTestRunInfoType {
@@ -199,6 +200,7 @@ extension DatadogSwiftTestingScopeProvider {
199200
var hasSuite: Bool { test.ddHasSuite }
200201
var isParameterized: Bool { testCase.isParameterized }
201202
var location: SwiftTestingSourceLocation { test.sourceLocation.asDD }
203+
var attachedTags: any TestTags { test.attachedTags }
202204

203205
var parameters: TestRunParameters {
204206
guard isParameterized else { return .init(arguments: .nil, metadata: nil) }

Sources/DatadogSDKTesting/TestImpactAnalysis/TestImpactAnalysisSkippabble.swift

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,37 @@
55
*/
66

77
import Foundation
8+
9+
struct TIASkippableTag: TestTag {
10+
typealias Value = Bool
11+
}
12+
13+
extension TestTag where Self == TIASkippableTag {
14+
static var tiaSkippable: Self { .init() }
15+
}
16+
17+
// XCTest
818
internal import XCTest
919

1020
public extension InstanceMethodTag where T: NSObject, V == Bool {
11-
static var tiaSkippable: Self { "tia.skippable" }
21+
static var tiaSkippable: Self { "dd.tia.skippable" }
1222
}
1323

1424
public extension TypeTag where T: NSObject, V == Bool {
15-
static var tiaSkippable: Self { "tia.skippable" }
25+
static var tiaSkippable: Self { "dd.tia.skippable" }
1626
}
1727

1828
public extension DynamicTag where V == Bool {
19-
static var tiaSkippableType: Self { Self(name: "tia.skippable", tagType: .forType) }
20-
static var tiaSkippableInstanceMethod: Self { Self(name: "tia.skippable", tagType: .instanceMethod) }
29+
static var tiaSkippableType: Self { Self(name: "dd.tia.skippable", tagType: .forType) }
30+
static var tiaSkippableInstanceMethod: Self { Self(name: "dd.tia.skippable", tagType: .instanceMethod) }
2131
}
2232

2333
@objc public extension DDTag {
2434
@objc static var tiaSkippableType: DDTag { DDTag(tag: .tiaSkippableType) }
2535
@objc static var tiaSkippableInstanceMethod: DDTag { DDTag(tag: .tiaSkippableInstanceMethod) }
2636
}
2737

28-
struct TIASkippableTag: XCTestTag {
29-
typealias Value = Bool
30-
38+
extension TIASkippableTag: XCTestTag {
3139
func parse(tags: borrowing TypeTags, test: String) -> Bool? {
3240
var isSuiteSkippable: Bool = true
3341
var isTestSkippable: Bool? = nil
@@ -41,6 +49,25 @@ struct TIASkippableTag: XCTestTag {
4149
}
4250
}
4351

44-
extension TestTag where Self == TIASkippableTag {
45-
static var tiaSkippable: Self { .init() }
52+
#if canImport(Testing)
53+
import Testing
54+
55+
extension Tag.dd {
56+
enum tia {
57+
@Tag static var skippable: Tag
58+
@Tag static var unskippable: Tag
59+
}
60+
}
61+
62+
extension TIASkippableTag: STTestTag {
63+
func parse(tags: borrowing Set<Tag>) -> Bool? {
64+
if tags.contains(.dd.tia.skippable) {
65+
return true
66+
}
67+
if tags.contains(.dd.tia.unskippable) {
68+
return false
69+
}
70+
return nil
71+
}
4672
}
73+
#endif

0 commit comments

Comments
 (0)