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 extensions API #868

Open
wants to merge 3 commits into
base: extensions-api
Choose a base branch
from
Open

Add extensions API #868

wants to merge 3 commits into from

Conversation

sol
Copy link
Member

@sol sol commented Nov 22, 2023

This PR adds an extension API that can be used to extend Hspec with additional functionality without the need to modify hspec-core.

An example extension that addresses #338:

module Test.Hspec.CI (use, only) where

import System.Environment

import Test.Hspec.Core.Extension
import Test.Hspec.Core.Extension.Item qualified as Item
import Test.Hspec.Core.Extension.Spec qualified as Spec
import Test.Hspec.Core.Extension.Tree qualified as Tree
import Test.Hspec.Core.Extension.Config qualified as Config

-- |
-- Do not run the spec items of the given subtree by default; execute them only
-- when the environment variable @CI@ is set.
--
-- It is idiomatic to use this in combination with `>>>`:
--
-- @
-- import Control.Arrow ((>>>))
-- import Test.Hspec
-- import qualified Test.Hspec.CI as CI
--
-- spec :: Spec
-- spec = do
--   describe ".." $ do
--     it ".." >>> CI.only $ do
--       ..
-- @
only :: SpecWith a -> SpecWith a
only = Spec.mapItems setCiOnly

data CiOnly = CiOnly

setCiOnly :: Item a -> Item a
setCiOnly = Item.setAnnotation CiOnly

getCiOnly :: Item a -> Maybe CiOnly
getCiOnly = Item.getAnnotation

newtype CiFlag = CI Bool

setCiFlag :: Bool -> Config -> Config
setCiFlag = Config.setAnnotation . CI

getCiFlag :: Config -> CiFlag
getCiFlag = fromMaybe (CI False) . Config.getAnnotation

use :: SpecWith a
use = do
  runIO (lookupEnv "CI") >>= \ case
    Nothing -> pass
    Just _ -> modifyConfig (setCiFlag True)
  registerOption $ flag "ci" setCiFlag "include itmes that are marked as \"CI only\""
  registerTransformation applyCiOnly

applyCiOnly :: Config -> [SpecTree] -> [SpecTree]
applyCiOnly config = case getCiFlag config of
  CI True -> id
  CI False -> Tree.mapItems $ \ item -> case getCiOnly item of
    Nothing -> item
    Just CiOnly -> Item.pendingWith message item
  where
    message = unlines [
        "This item is marked as \"CI only\" and excluded by default."
      , "Use `--ci' to include this item."
      ]

@sol sol force-pushed the tags branch 30 times, most recently from c0c2751 to 8bea089 Compare November 24, 2023 06:46
@sol sol force-pushed the tags branch 2 times, most recently from 7503c8d to 0aac23c Compare May 29, 2024 12:09
@sol sol changed the title Add a way to mark slow tests and exclude them by default Add extensions API May 29, 2024
@sol sol marked this pull request as ready for review May 29, 2024 12:12
@sol sol mentioned this pull request May 29, 2024
@BebeSparkelSparkel
Copy link

I think it would be helpful to have a specific environment lookup function or even better to have flag, option, ... automatically do an environment lookup. Either should have the standard HSPEC_ prefix.

@sol
Copy link
Member Author

sol commented May 29, 2024

I think it would be helpful to have a specific environment lookup function or even better to have flag, option, ... automatically do an environment lookup. Either should have the standard HSPEC_ prefix.

I think that should already be the case, according to the rules documented here: https://hspec.github.io/options.html#specifying-options-through-environment-variables

@sol
Copy link
Member Author

sol commented May 29, 2024

The reason that the example specifically looks up the CI environment variable is because this environment variable is already set for GitHub actions, but setting HSPEC_CI=yes will work as well.

@BebeSparkelSparkel
Copy link

It would be nice if the Test.Hspec.Core.Extension.option interface did not take the type OptionSetter and instead have interface functions for the common cases.

@sol
Copy link
Member Author

sol commented May 29, 2024

It would be nice if the Test.Hspec.Core.Extension.option interface did not take the type OptionSetter and instead have interface functions for the common cases.

How exactly would that look like?

Right now you can say things like

registerOption $ flag "ci" setCiFlag
    "include itmes that are marked as \"CI only\""

or

registerOption $ option "tags" (argument "TAGS" return addTagFilters)
    "TAGS can be a list of tag names."

How would you want to express this instead?

@BebeSparkelSparkel
Copy link

In my pr I used the below

  , option "timeout" (argument "N" (fmap Seconds . readMaybe) (setTimeout . Just)) "each test will run at most N seconds"
   , mkOptionNoArg "no-timeout" Nothing (setTimeout Nothing) "remove test time limits"

So, it would be useful to have

optionalArg :: Read a => String -> String -> (a -> Config -> Config) -> Option
optionNoArg :: String -> (Config -> Config) -> Option

these are more clear than trying to figure out which constructor to use with option.

option and argument could then be moved to GetOpt.Declarative.Types for more advanced uses.

@sol
Copy link
Member Author

sol commented May 29, 2024

mkOptionNoArg "no-timeout" Nothing (setTimeout Nothing) "remove test time limits"

I think this is currently not covered by the API, I'll try to look into it tomorrow.

Copy link

@BebeSparkelSparkel BebeSparkelSparkel left a comment

Choose a reason for hiding this comment

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

Just some thoughts

hspec-core/src/Test/Hspec/Core/Config/Definition.hs Outdated Show resolved Hide resolved
argument :: String -> (String -> Maybe a) -> (a -> Config -> Config) -> OptionSetter
argument name parser setter = OptionSetter (Core.argument name parser setter)

registerOption :: HasCallStack => Option -> SpecWith a

Choose a reason for hiding this comment

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

It seems that registerOption will be used every time flag and option are used. I know you want to use them as internal functions but it would be better for the extension writers to have them return SpecWith a instead of Option.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's now registerOptions that expects a list of Option values.

Keeping the Option type Might allow for reuse of Test.Hspec.Core.Extension.Option for #870. So let's not abandon it, at least not yet.

Choose a reason for hiding this comment

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

Ok sounds good.

flag :: String -> (Bool -> Config -> Config) -> String -> Option
flag name setter = Option . Core.flag name setter

option :: String -> OptionSetter -> String -> Option

Choose a reason for hiding this comment

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

OptionSetter makes this hard to use and it would be better just have interface functions for all the common cases like

flag :: String -> Maybe String -> (Bool -> Config -> Config) -> String -> Option
optionalArg :: Read a => String -> Maybe String -> String -> (a -> Config -> Config) -> String -> Option
optionNoArg :: String -> Maybe String -> (Config -> Config) -> String -> Option
argument :: Read a => String -> (a -> Config -> Config) -> String -> Option

these are more clear than trying to figure out which constructor to use with option.

option and argument could then be moved to GetOpt.Declarative.Types for more advanced uses.

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member Author

@sol sol Jun 6, 2024

Choose a reason for hiding this comment

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

I'm not 100% sold on it, as it's not as clear anymore, which string is the option name, and which string is the name for the parameter. But let's give this a try, provisionally.

Choose a reason for hiding this comment

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

You are right about the which string is which. Perhaps aliases would be helpful.

argument :: CliName -> CliArgName -> CliHelp -> (Bool -> Config -> Config) -> Option
type CliName = String
type CliArgName = String
type CliHelp = String

I rearranged the arguments because I like having functions at the end which allows multiline lambdas without parenthaseses and hanging arguments. This should be applied to all the option functions if you choose to use it.


import Test.Hspec.Core.Extension.Tree (SpecTree)

data Config = Config {

Choose a reason for hiding this comment

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

It seems strange to me to have 2 Config types especially when the other is not exported.

Also, since this is exported it locks in a lot of options in core that could actually be extensions. For example smallcheck is being deprecated.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think it's possible to remove some of those options after #870. However, only from this Config type. The fields of the core Config can only be deprecated, not removed, for backwards compatibility.

That's also the reason why I duplicated the type, so that the extension API doesn't "know about" legacy stuff.

It probably makes sense to finish #870 before the extensions API lands on main.

That's the approach I'm taking for now:

This PR is now against #894, with the options and documentation changes.

Choose a reason for hiding this comment

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

Thank your for outlining the dev plan for extensions. Is there anything you want me to specifcally look at?

hspec-core/src/Test/Hspec/Core/Extension.hs Outdated Show resolved Hide resolved
hspec-core/src/Test/Hspec/Core/Extension/Item.hs Outdated Show resolved Hide resolved
hspec-core/src/Test/Hspec/Core/Extension/Spec.hs Outdated Show resolved Hide resolved
hspec-core/src/Test/Hspec/Core/Extension/Tree.hs Outdated Show resolved Hide resolved
@sol sol closed this May 30, 2024
@sol sol deleted the tags branch May 30, 2024 04:22
@sol sol restored the tags branch May 30, 2024 04:23
@sol sol reopened this May 30, 2024
@sol sol force-pushed the tags branch 4 times, most recently from a6b7401 to c0bc302 Compare May 30, 2024 09:52
@sol sol changed the base branch from main to extensions-api May 30, 2024 16:46
@BebeSparkelSparkel BebeSparkelSparkel mentioned this pull request Jun 6, 2024
2 tasks
@sol sol changed the base branch from extensions-api to main June 12, 2024 13:43
@sol sol changed the base branch from main to extensions-api June 12, 2024 13:44
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.

None yet

2 participants