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

Add a random sample utility #553

Merged
merged 11 commits into from
Sep 30, 2024
Merged

Add a random sample utility #553

merged 11 commits into from
Sep 30, 2024

Conversation

zachmargolis
Copy link
Contributor

@zachmargolis zachmargolis commented Sep 25, 2024

I happened to look at #551 and saw that we were copy-pasting a pattern used many times that is arr[Math.floor(Math.random() * arr.length]) and I figured, what a great opportunity to Not Repeat ourselves.

Hubot had a msg.random helper so I modeled this after that. Open to renaming it as well

If we like this, I am happy to update the wiki to suggest it! Or add a lint if we want too.

I'm also looking for help with the test suite, some of the cases seem to be returning undefined in ways that I don't expect and my attempts to print-debug were not successful


Checklist:

  • Code has been formatted with prettier
  • The OAuth wiki
    page has been updated if Charlie needs any new OAuth events or scopes
  • The Environment Variables
    wiki page has been updated if new environment variables were introduced
    or existing ones changed
  • The dev wiki has been updated, e.g.:
    • local development processes have changed
    • major development workflows have changed
    • internal utilities or APIs have changed
    • testing or deployment processes have changed
  • If appropriate, the NIST 800-218 documentation has been updated

@zachmargolis zachmargolis requested a review from a team as a code owner September 25, 2024 16:25
@mgwalker
Copy link
Member

I think you'll need to replace Math.random mocks with sample mocks. Everything imported/exported by the utils/test.js module is mocked, and since it's pulling in sample, then where the code-under-test is using sample, it's not getting a value back because the mock isn't... well... mocked.

So for example, the random-response.test.js file has this:

const random = jest.spyOn(global.Math, "random");

...

random.mockReturnValue(0);

This would likely be replaced with something like:

const {
  axios,
  getApp,
  utils: { cache, sample },
} = require("../utils/test");

...

sample.mockReturnValue({ ...the object that should be returned from the list of options });

Alternatively, you could remove sample from the test utilities, I think. Mocking the sample method means changing all those .mockReturnValue(0) to .mockReturnValue({ object }). Part of me thinks, "gosh that's a lot of extra work" while another part of me thinks "gosh that makes the tests so much more explicit."

Either way you decide to go, this is a nice addition!

@zachmargolis
Copy link
Contributor Author

I think you'll need to replace Math.random mocks with sample mocks. Everything imported/exported by the utils/test.js module is mocked, and since it's pulling in sample, then where the code-under-test is using sample, it's not getting a value back because the mock isn't... well... mocked.

Ok thanks! I removed from utils/test.js and am still getting some errors I got before, where it looks like the sample function just returns undefined everywhere? Such as in pugs.test.js which does no explicit mocking of random or anything, any pointers for how to trace that one down? Is there another level of mocking/stubbing of dependencies going on?

@zachmargolis
Copy link
Contributor Author

I think you'll need to replace Math.random mocks with sample mocks. Everything imported/exported by the utils/test.js module is mocked, and since it's pulling in sample, then where the code-under-test is using sample, it's not getting a value back because the mock isn't... well... mocked.

Ok thanks! I removed from utils/test.js and am still getting some errors I got before, where it looks like the sample function just returns undefined everywhere? Such as in pugs.test.js which does no explicit mocking of random or anything, any pointers for how to trace that one down? Is there another level of mocking/stubbing of dependencies going on?

Ok so I realize it's this line:

jest.mock("./index");

So now anything that does a require like this:

const  { sample } = require('./utils');

will get a function that just returns undefined, would it be bad if I updated scripts to import directly, like

const sample = require('./utils/sample');

and avoid it getting mocked out like that?

@mgwalker
Copy link
Member

I think we can mark the CodeQL failure as a false positive. There's nothing security-sensitive about how we're using randomness here.

@zachmargolis
Copy link
Contributor Author

I think we can mark the CodeQL failure as a false positive. There's nothing security-sensitive about how we're using randomness here.

Thanks! I think all the code is ready for final review now, got specs passing.

Copy link
Member

@mgwalker mgwalker left a comment

Choose a reason for hiding this comment

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

Excellent

@@ -5,6 +5,7 @@ const {
helpMessage,
stats: { incrementStats },
} = require("../utils");
const sample = require("../utils/sample");
Copy link
Member

Choose a reason for hiding this comment

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

To your question in the PR, this seems fine! I don't think it's my ideal case, but it's better than fighting with the tests forever. Maybe someday we can come back and find a way to make the tests work properly, but this is totally within the realm of "good enough," which is all anything needs to be. 😂

@@ -38,7 +39,7 @@ module.exports = (app) => {
}
});

const joke = jokes[Math.floor(Math.random() * jokes.length)];
const joke = sample(jokes);
Copy link
Member

Choose a reason for hiding this comment

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

omg I love this so much more.

* @param {Number} [randomValue] random value that can be injected for more deterministic behavior
* @return {T=} an item from the array (or undefined if empty array)
*/
function sample(arr, randomValue = Math.random()) {
Copy link
Member

Choose a reason for hiding this comment

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

I had to double-check when default arguments are evaluated. I think in Python they're evaluated at definition time, in which case every invocation of sample would get the same value. But you're correct, in Javascript they're evaluated at call time. 🎉

Comment on lines +6 to +11
it("selects an item from an array based on randomValue and clamps it to the array bounds", () => {
expect(sample(arr, 0)).toEqual("a");
expect(sample(arr, 0.999)).toEqual("e");
expect(sample(arr, -1)).toEqual("a");
expect(sample(arr, 100)).toEqual("e");
});
Copy link
Member

Choose a reason for hiding this comment

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

🤩

@zachmargolis zachmargolis merged commit f49327a into main Sep 30, 2024
7 checks passed
@zachmargolis zachmargolis deleted the margolis-dry-random branch September 30, 2024 15:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants