Skip to content

[12428] CustomService is run twice#12430

Merged
christian-bromann merged 45 commits intowebdriverio:mainfrom
jemishgopani:fix/duplicate-service-run
Apr 24, 2024
Merged

[12428] CustomService is run twice#12430
christian-bromann merged 45 commits intowebdriverio:mainfrom
jemishgopani:fix/duplicate-service-run

Conversation

@jemishgopani
Copy link
Copy Markdown
Contributor

@jemishgopani jemishgopani commented Mar 5, 2024

Proposed changes

12428

Types of changes

  • Polish (an improvement for an existing feature)
  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update (improvements to the project's docs)
  • Specification changes (updates to WebDriver command specifications)
  • Internal updates (everything related to internal scripts, governance documentation and CI files)

Checklist

  • I have read the CONTRIBUTING doc
  • I have added tests that prove my fix is effective or that my feature works
  • I have added the necessary documentation (if appropriate)
  • I have added proper type definitions for new commands (if appropriate)

Backport Request

Further comments

  • Removed args from the constructor because we are already initializing args in the run method.
  • As always I'll add backport PR if this PR changes look good to maintainers.

Reviewers: @webdriverio/project-committers

@linux-foundation-easycla
Copy link
Copy Markdown

linux-foundation-easycla bot commented Mar 5, 2024

CLA Signed

The committers listed above are authorized under a signed CLA.

@jemishgopani jemishgopani force-pushed the fix/duplicate-service-run branch from ac61b12 to 512088e Compare March 5, 2024 03:34
@christian-bromann
Copy link
Copy Markdown
Member

@jemishgopani thanks for the patch, mind taking a look at the failing smoke tests? I think your solution fails for when you pass in command line arguments e.g. npx wdio run wdio.conf.js --spec foo

@jemishgopani jemishgopani marked this pull request as draft March 5, 2024 15:34
@jemishgopani jemishgopani marked this pull request as ready for review March 6, 2024 05:29
@jemishgopani
Copy link
Copy Markdown
Contributor Author

@christian-bromann I have tried to fix please take a look and provide your feedback.

@jemishgopani jemishgopani marked this pull request as draft March 7, 2024 01:30
@jemishgopani jemishgopani marked this pull request as ready for review March 9, 2024 12:11
Copy link
Copy Markdown
Member

@christian-bromann christian-bromann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jemishgopani can we merge what happens in mergeReportersAndServices into the custom merge function? I think there is no need to have an extra function to merge reporters and services, instead we can just use the custom merger

@jemishgopani
Copy link
Copy Markdown
Contributor Author

jemishgopani commented Mar 30, 2024

Hi @christian-bromann
Can you please have a look at the changes?

Copy link
Copy Markdown
Member

@christian-bromann christian-bromann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jemishgopani thanks for adding a test. I reviewed the changes and it doesn't seem to fix the overall issue. Let me explain:

  • I tried to verify whether removing your changes in packages/wdio-config/src/node/ConfigParser.ts will make the test break, unfortunately it didn't
  • then I tried to verify that the issue is reproducible in the unit test, which it is, for that I applied this change:
diff --git a/packages/wdio-config/tests/node/configparser.test.ts b/packages/wdio-config/tests/node/configparser.test.ts
index d2ff81ce0..33f170fb4 100644
--- a/packages/wdio-config/tests/node/configparser.test.ts
+++ b/packages/wdio-config/tests/node/configparser.test.ts
@@ -13,7 +13,6 @@ import MockFileContentBuilder from '../lib/MockFileContentBuilder.js'
 import type { FilePathsAndContents, MockSystemFilePath, MockSystemFolderPath } from '../lib/MockPathService.js'
 import ConfigParserBuilder from '../lib/ConfigParserBuilder.js'
 import { FileNamed, realReadFilePair, realRequiredFilePair } from '../lib/FileNamed.js'
-import TestCustomService from '../__fixtures__/test-custom-service.js'
 
 const log = logger('')
 const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
@@ -41,6 +40,8 @@ vi.mock('@babel/register', () => ({
     default: vi.fn()
 }))
 
+class CustomService {}
+
 /**
  * Entirely in memory config structure to avoid reading the file system at all
  *
@@ -70,7 +71,8 @@ const TestWdioConfig_AllInMemory = (): MockFileContent => ({
                 path.join(root, '/tests/validateConfig.test.ts'),
                 path.join(root, '/tests/..', 'src/index.ts')
             ]
-        }
+        },
+        services: [[CustomService, { foo: 'bar' }]],
     }
 })
 
@@ -691,26 +693,24 @@ describe('ConfigParser', () => {
             expect(configParser.getCapabilities()).toMatchObject([{ browserName: 'safari' }])
         })
 
-        it('should allow to specifying services', async () => {
+        it.only('should allow to specifying services', async () => {
             const configParser = await ConfigParserForTest(FIXTURES_CONF)
             await configParser.initialize({
                 services: [
-                    [TestCustomService, { timestamp: 1709633731716 }]
+                    [CustomService, { foo: 'bar' }]
                 ]
             })
 
-            const services = (await configParser.getConfig())['services']
+            const services = configParser.getConfig().services
             expect(services).toHaveLength(1)
             expect(services[0][1].timestamp).toBe(1709633731716)
-            expect(typeof services[0][0]).toBe(typeof TestCustomService)
+            expect(typeof services[0][0]).toBe(typeof CustomService)
         })
 
         it('should allow to specifying reporters', async () => {
             const configParser = await ConfigParserForTest(FIXTURES_CONF)
             await configParser.initialize({
-                reporters: [
-                    ['dot', 'spec']
-                ]
+                reporters: ['dot', 'spec']
             })
 
             const reporters = (await configParser.getConfig())['reporters']

Note: the unit tests load config files from memory and not from the actual file system. So there isn't a need for test-custom-service.js.

I would suggest creating the custom merge as following: merge the service and reporter arrays by removing all entry objects from one side, e.g.:

diff --git a/packages/wdio-config/src/node/ConfigParser.ts b/packages/wdio-config/src/node/ConfigParser.ts
index f828b3df3..f4f6302d9 100644
--- a/packages/wdio-config/src/node/ConfigParser.ts
+++ b/packages/wdio-config/src/node/ConfigParser.ts
@@ -2,7 +2,7 @@ import path from 'node:path'
 
 import logger from '@wdio/logger'
 import { deepmerge, deepmergeCustom } from 'deepmerge-ts'
-import type { Capabilities, Options, Services } from '@wdio/types'
+import type { Capabilities, Options, Reporters, Services } from '@wdio/types'
 
 import RequireLibrary from './RequireLibrary.js'
 import FileSystemPathService from './FileSystemPathService.js'
@@ -13,6 +13,8 @@ import { SUPPORTED_HOOKS, SUPPORTED_FILE_EXTENSIONS, DEFAULT_CONFIGS, NO_NAMED_C
 import type { PathService, ModuleImportService } from '../types.js'
 
 const log = logger('@wdio/config:ConfigParser')
+const MERGE_DUPLICATION = ['services', 'reporters'] as const
+type KeyWithMergeDuplication = (typeof MERGE_DUPLICATION)[number]
 
 type Spec = string | string[]
 type ESMImport = { config?: TestrunnerOptionsWithParameters }
@@ -182,12 +184,11 @@ export default class ConfigParser {
          * Add deepmergeCustom to remove array('services', 'reporters', 'capabilities') duplication in the config object
          */
         const customDeepMerge = deepmergeCustom({
-            mergeArrays: (values, utils, meta) => {
-                if (meta && ['services', 'reporters', 'capabilities'].includes(meta.key as string)) {
-                    const mergedArr = deepmerge(this._config[meta.key as keyof WebdriverIO.Config], object[meta.key as keyof WebdriverIO.Config]) as []
-                    return mergedArr.filter((item, index, merged) => {
-                        return merged.indexOf(item) === index
-                    })
+            mergeArrays: ([oldValue, newValue], utils, meta) => {
+                const key = meta?.key as KeyWithMergeDuplication
+                if (meta && MERGE_DUPLICATION.includes(key)) {
+                    const origWithoutObjectEntries = oldValue.filter((value: [Services.ServiceClass, WebdriverIO.ServiceOption] | [Reporters.ReporterClass, WebdriverIO.ReporterOption]) => typeof value[0] === 'object')
+                    return new Set(deepmerge(newValue, origWithoutObjectEntries))
                 }
                 return utils.actions.defaultMerge
             }
diff --git a/packages/wdio-config/tests/node/configparser.test.ts b/packages/wdio-config/tests/node/configparser.test.ts
index d2ff81ce0..d22c9ad04 100644
--- a/packages/wdio-config/tests/node/configparser.test.ts
+++ b/packages/wdio-config/tests/node/configparser.test.ts
@@ -13,7 +13,6 @@ import MockFileContentBuilder from '../lib/MockFileContentBuilder.js'
 import type { FilePathsAndContents, MockSystemFilePath, MockSystemFolderPath } from '../lib/MockPathService.js'
 import ConfigParserBuilder from '../lib/ConfigParserBuilder.js'
 import { FileNamed, realReadFilePair, realRequiredFilePair } from '../lib/FileNamed.js'
-import TestCustomService from '../__fixtures__/test-custom-service.js'
 
 const log = logger('')
 const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
@@ -41,6 +40,8 @@ vi.mock('@babel/register', () => ({
     default: vi.fn()
 }))
 
+class CustomServiceOrReporter {}
+
 /**
  * Entirely in memory config structure to avoid reading the file system at all
  *
@@ -70,7 +71,9 @@ const TestWdioConfig_AllInMemory = (): MockFileContent => ({
                 path.join(root, '/tests/validateConfig.test.ts'),
                 path.join(root, '/tests/..', 'src/index.ts')
             ]
-        }
+        },
+        services: ['appium', [CustomServiceOrReporter, { foo: 'bar' }]],
+        reporters: ['spec', [CustomServiceOrReporter, { foo: 'bar' }]],
     }
 })
 
@@ -691,33 +694,48 @@ describe('ConfigParser', () => {
             expect(configParser.getCapabilities()).toMatchObject([{ browserName: 'safari' }])
         })
 
-        it('should allow to specifying services', async () => {
+        it('should ensure there we do not duplicate class entries', async () => {
             const configParser = await ConfigParserForTest(FIXTURES_CONF)
             await configParser.initialize({
                 services: [
-                    [TestCustomService, { timestamp: 1709633731716 }]
-                ]
-            })
-
-            const services = (await configParser.getConfig())['services']
-            expect(services).toHaveLength(1)
-            expect(services[0][1].timestamp).toBe(1709633731716)
-            expect(typeof services[0][0]).toBe(typeof TestCustomService)
-        })
-
-        it('should allow to specifying reporters', async () => {
-            const configParser = await ConfigParserForTest(FIXTURES_CONF)
-            await configParser.initialize({
+                    'appium',
+                    'sauce',
+                    [CustomServiceOrReporter, { foo: 'bar' }]
+                ],
                 reporters: [
-                    ['dot', 'spec']
+                    'dot',
+                    'spec',
+                    [CustomServiceOrReporter, { foo: 'bar' }] as any
                 ]
             })
 
-            const reporters = (await configParser.getConfig())['reporters']
-            expect(reporters).toHaveLength(1)
-            expect(reporters[0]).toHaveLength(2)
-            expect(reporters[0][0]).toBe('dot')
-            expect(reporters[0][1]).toBe('spec')
+            const { services, reporters } = configParser.getConfig()
+            expect(services).toHaveLength(3)
+            expect(services).toMatchInlineSnapshot(`
+              Set {
+                "appium",
+                "sauce",
+                [
+                  [Function],
+                  {
+                    "foo": "bar",
+                  },
+                ],
+              }
+            `)
+            expect(reporters).toHaveLength(3)
+            expect(reporters).toMatchInlineSnapshot(`
+              Set {
+                "dot",
+                "spec",
+                [
+                  [Function],
+                  {
+                    "foo": "bar",
+                  },
+                ],
+              }
+            `)
         })
     })

What do you think?

@jemishgopani
Copy link
Copy Markdown
Contributor Author

@christian-bromann Thanks 🙏

changes seems looking good to me I'll implement this and update this PR soon.

@jemishgopani jemishgopani marked this pull request as draft April 3, 2024 12:16
@jemishgopani jemishgopani marked this pull request as ready for review April 5, 2024 01:02
@christian-bromann
Copy link
Copy Markdown
Member

@jemishgopani let me know if there is anything I can do to help.

@jemishgopani
Copy link
Copy Markdown
Contributor Author

@christian-bromann I have made some changes to your suggested code and updated the code. Whenever you have time please review this PR.

Copy link
Copy Markdown
Member

@christian-bromann christian-bromann left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

@christian-bromann
Copy link
Copy Markdown
Member

@jemishgopani mind raising the same PR for the v8 branch?

@christian-bromann christian-bromann added the PR: Bug Fix 🐛 PRs that contain bug fixes label Apr 20, 2024
@christian-bromann christian-bromann merged commit af6b6b6 into webdriverio:main Apr 24, 2024
@EdmundasD EdmundasD mentioned this pull request May 17, 2024
13 tasks
@EdmundasD EdmundasD mentioned this pull request Jul 23, 2024
13 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

PR: Bug Fix 🐛 PRs that contain bug fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants