Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[dotnet] Gracefully handle clashing device names #14713

Merged
merged 9 commits into from
Nov 5, 2024

Conversation

RenderMichael
Copy link
Contributor

@RenderMichael RenderMichael commented Nov 5, 2024

User description

Thanks for contributing to Selenium!
A PR well described will help maintainers to quickly review and merge it

Before submitting your PR, please check our contributing guidelines.
Avoid large PRs, help reviewers by making them as simple and short as possible.

Description

This handles the scenario where you request names clash between different types of devices.

Motivation and Context

Today, this throws an unhelpful InvalidCastException, with no indication that the user did anything wrong. Now, it throws an InvalidOperationException (an exception whose meaning is "user cannot do this") with a better message.

Types of changes

  • Bug fix (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 change)

Checklist

  • I have read the contributing document.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

PR Type

Bug fix, Tests


Description

  • Improved exception handling for scenarios where device names clash, replacing InvalidCastException with InvalidOperationException for better clarity.
  • Added a new method FindDeviceById to streamline device lookup and reduce code duplication.
  • Enhanced test coverage by adding tests to verify the correct exceptions are thrown when device names clash.
  • Updated exception messages to provide more informative feedback to the user.

Changes walkthrough 📝

Relevant files
Bug fix
Actions.cs
Handle clashing device names with improved exception handling

dotnet/src/webdriver/Interactions/Actions.cs

  • Added exception handling for clashing device names.
  • Introduced FindDeviceById method for device lookup.
  • Replaced direct field assignment with property initialization.
  • Improved exception messages for invalid device operations.
  • +33/-57 
    Tests
    CombinedInputActionsTest.cs
    Add tests for clashing device name handling                           

    dotnet/test/common/Interactions/CombinedInputActionsTest.cs

  • Added tests for handling clashing device names.
  • Verified exception messages for invalid device operations.
  • +33/-0   

    💡 PR-Agent usage: Comment /help "your question" on any pull request to receive relevant information

    Copy link
    Contributor

    qodo-merge-pro bot commented Nov 5, 2024

    PR Reviewer Guide 🔍

    Here are some key observations to aid the review process:

    ⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
    🧪 PR contains tests
    🔒 No security concerns identified
    ⚡ Recommended focus areas for review

    Exception Handling
    Verify that the new exception handling for clashing device names is comprehensive and provides clear error messages to users.

    Code Duplication
    Check if the similar code structure in SetActivePointer, SetActiveKeyboard, and SetActiveWheel methods can be refactored to reduce duplication.

    Performance Consideration
    Evaluate if the FindDeviceById method's linear search through sequences can be optimized for better performance, especially with a large number of sequences.

    Copy link
    Contributor

    qodo-merge-pro bot commented Nov 5, 2024

    PR Code Suggestions ✨

    Explore these optional code suggestions:

    CategorySuggestion                                                                                                                                    Score
    Maintainability
    Refactor device activation methods to reduce code duplication

    Consider implementing a generic method for setting active devices to reduce code
    duplication across SetActivePointer, SetActiveKeyboard, and SetActiveWheel methods.

    dotnet/src/webdriver/Interactions/Actions.cs [76-138]

    -public Actions SetActivePointer(PointerKind kind, string name)
    +private T SetActiveDevice<T>(string name, Func<string, T> createDevice) where T : InputDevice
     {
         InputDevice device = FindDeviceById(name);
     
         if (device == null)
         {
    -        this.activePointer = new PointerInputDevice(kind, name);
    -    }
    -    else
    -    {
    -        this.activePointer = device as PointerInputDevice
    -            ?? throw new InvalidOperationException($"Device under the name \"{name}\" is not a pointer. Actual input type: {device.DeviceKind}");
    +        return createDevice(name);
         }
     
    +    return device as T ?? throw new InvalidOperationException($"Device under the name \"{name}\" is not a {typeof(T).Name}. Actual input type: {device.DeviceKind}");
    +}
    +
    +public Actions SetActivePointer(PointerKind kind, string name)
    +{
    +    this.activePointer = SetActiveDevice(name, n => new PointerInputDevice(kind, n));
         return this;
     }
     
     public Actions SetActiveKeyboard(string name)
     {
    -    InputDevice device = FindDeviceById(name);
    -
    -    if (device == null)
    -    {
    -        this.activeKeyboard = new KeyInputDevice(name);
    -    }
    -    else
    -    {
    -        this.activeKeyboard = device as KeyInputDevice
    -            ?? throw new InvalidOperationException($"Device under the name \"{name}\" is not a keyboard. Actual input type: {device.DeviceKind}");
    -
    -    }
    -
    +    this.activeKeyboard = SetActiveDevice(name, n => new KeyInputDevice(n));
         return this;
     }
     
     public Actions SetActiveWheel(string name)
     {
    -    InputDevice device = FindDeviceById(name);
    -
    -    if (device == null)
    -    {
    -        this.activeWheel = new WheelInputDevice(name);
    -    }
    -    else
    -    {
    -        this.activeWheel = device as WheelInputDevice
    -            ?? throw new InvalidOperationException($"Device under the name \"{name}\" is not a wheel. Actual input type: {device.DeviceKind}");
    -    }
    -
    +    this.activeWheel = SetActiveDevice(name, n => new WheelInputDevice(n));
         return this;
     }
    • Apply this suggestion
    Suggestion importance[1-10]: 9

    Why: This suggestion significantly improves code maintainability by reducing duplication across multiple methods. It introduces a generic method for setting active devices, which makes the code more concise and easier to maintain, especially if new device types are added in the future.

    9
    Performance
    Implement caching for input devices to improve lookup performance

    Consider using a dictionary to store and retrieve input devices by their names,
    instead of searching through the action sequences each time. This would improve
    performance, especially for repeated operations on the same devices.

    dotnet/src/webdriver/Interactions/Actions.cs [140-155]

    +private Dictionary<string, InputDevice> deviceCache = new Dictionary<string, InputDevice>();
    +
     private InputDevice FindDeviceById(string name)
     {
    -    foreach (var sequence in this.actionBuilder.ToActionSequenceList())
    +    if (deviceCache.TryGetValue(name, out var device))
         {
    -        Dictionary<string, object> actions = sequence.ToDictionary();
    -
    -        string id = (string)actions["id"];
    -
    -        if (id == name)
    -        {
    -            return sequence.inputDevice;
    -        }
    +        return device;
         }
     
    -    return null;
    +    device = this.actionBuilder.ToActionSequenceList()
    +        .FirstOrDefault(sequence => (string)sequence.ToDictionary()["id"] == name)?.inputDevice;
    +
    +    if (device != null)
    +    {
    +        deviceCache[name] = device;
    +    }
    +
    +    return device;
     }
    • Apply this suggestion
    Suggestion importance[1-10]: 8

    Why: This suggestion significantly improves performance by implementing a caching mechanism for input devices. It reduces repeated searches through action sequences, which is especially beneficial for operations involving the same devices multiple times.

    8
    Enhancement
    ✅ Use pattern matching to simplify device type checking and assignment
    Suggestion Impact:The suggestion to use pattern matching instead of if-else statements for setting active devices was implemented in the commit. The code now uses a switch expression for device assignment.

    code diff:

    @@ -77,15 +77,12 @@
             {
                 InputDevice device = FindDeviceById(name);
     
    -            if (device == null)
    -            {
    -                this.activePointer = new PointerInputDevice(kind, name);
    -            }
    -            else
    -            {
    -                this.activePointer = device as PointerInputDevice
    -                    ?? throw new InvalidOperationException($"Device under the name \"{name}\" is not a pointer. Actual input type: {device.DeviceKind}");
    -            }
    +            this.activePointer = device switch
    +            {
    +                null => new PointerInputDevice(kind, name),
    +                PointerInputDevice pointerDevice => pointerDevice,
    +                _ => throw new InvalidOperationException($"Device under the name \"{name}\" is not a pointer. Actual input type: {device.DeviceKind}"),
    +            };

    Consider using a switch statement or pattern matching instead of multiple if-else
    statements when setting active devices. This would make the code more readable and
    easier to maintain, especially if more device types are added in the future.

    dotnet/src/webdriver/Interactions/Actions.cs [76-91]

     public Actions SetActivePointer(PointerKind kind, string name)
     {
         InputDevice device = FindDeviceById(name);
     
    -    if (device == null)
    +    this.activePointer = device switch
         {
    -        this.activePointer = new PointerInputDevice(kind, name);
    -    }
    -    else
    -    {
    -        this.activePointer = device as PointerInputDevice
    -            ?? throw new InvalidOperationException($"Device under the name \"{name}\" is not a pointer. Actual input type: {device.DeviceKind}");
    -    }
    +        null => new PointerInputDevice(kind, name),
    +        PointerInputDevice pointer => pointer,
    +        _ => throw new InvalidOperationException($"Device under the name \"{name}\" is not a pointer. Actual input type: {device.DeviceKind}")
    +    };
     
         return this;
     }
    • Apply this suggestion
    Suggestion importance[1-10]: 6

    Why: The suggestion enhances code readability and maintainability by using pattern matching instead of if-else statements. While it's a good improvement, it's not as critical as the performance enhancement in the first suggestion.

    6

    💡 Need additional feedback ? start a PR chat

    @nvborisenko
    Copy link
    Member

    nvborisenko commented Nov 5, 2024

    This is the first time when I see suggestions from codium seems reasonable, @RenderMichael is it? (except caching)

    @RenderMichael
    Copy link
    Contributor Author

    The pattern matching is actually a good suggestion! The maintainability one seems like overkill, especially with the lambda expression. Let me implement the pattern matching

    @RenderMichael
    Copy link
    Contributor Author

    @nvborisenko changes are implemented ✅

    @nvborisenko
    Copy link
    Member

    CI is failing:

    => OpenQA.Selenium.Interactions.CombinedInputActionsTest.ShouldHandleClashingDeviceNamesGracefully
    18:53:37.025 DEBUG HttpCommandExecutor: Executing command: [a6230dfe45be69c230459f8b9ff07225]: cancelActions {}
    18:53:37.025 TRACE HttpCommandExecutor: >> DELETE RequestUri: http://localhost:36619/session/a6230dfe45be69c230459f8b9ff07225/actions, Content: null, Headers: 2
    18:53:37.027 TRACE HttpCommandExecutor: << StatusCode: 200, ReasonPhrase: OK, Content: System.Net.Http.HttpConnectionResponseContent, Headers: 1
    18:53:37.028 DEBUG HttpCommandExecutor: Response: ( Success: )
    Stack overflow.
    Repeat 174501 times:
    --------------------------------
       at OpenQA.Selenium.Interactions.ActionSequence.get_inputDevice()
    --------------------------------
       at OpenQA.Selenium.Interactions.Actions.FindDeviceById(System.String)
       at OpenQA.Selenium.Interactions.Actions.SetActiveWheel(System.String)
       at OpenQA.Selenium.Interactions.CombinedInputActionsTest+<>c__DisplayClass18_0.<ShouldHandleClashingDeviceNamesGracefully>b__0()
    

    @RenderMichael
    Copy link
    Contributor Author

    @nvborisenko That's a separate bug I found. Not sure why it's failing now

    Can I include a fix in this PR, or a separate one that would need to get merged first?

    @RenderMichael
    Copy link
    Contributor Author

    Ah I understand now. I have the bug fixed locally, so it was passing for me. Also, I don't think any other test exercised this code path.

    @nvborisenko
    Copy link
    Member

    Tests coverage is another topic, we even don't know what is covered. But please at least for your "simple" changes, given that you already added a test, let's keep it workable in this PR.

    @RenderMichael
    Copy link
    Contributor Author

    The unrelated bug that this caught has been fixed in this PR

    @RenderMichael
    Copy link
    Contributor Author

    CI is clean now, let me know if more changes are needed!

    Copy link
    Member

    @nvborisenko nvborisenko left a comment

    Choose a reason for hiding this comment

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

    Thank you @RenderMichael !

    @nvborisenko nvborisenko merged commit a9ec9ca into SeleniumHQ:trunk Nov 5, 2024
    10 checks passed
    @RenderMichael RenderMichael deleted the device-clash branch November 5, 2024 21:30
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Projects
    None yet
    Development

    Successfully merging this pull request may close these issues.

    2 participants