Compare commits

...

3 Commits

Author SHA1 Message Date
Patrick Stevens
afc952241d Add docstring to generated mock (#95) 2024-02-13 23:19:47 +00:00
Patrick Stevens
c3af52596f Permit public mocks (#94) 2024-02-13 19:58:30 +00:00
Patrick Stevens
8bd13c0bb4 Add open statements in generated mocks (#93) 2024-02-13 19:26:50 +00:00
6 changed files with 135 additions and 17 deletions

View File

@@ -5,6 +5,8 @@
namespace SomeNamespace namespace SomeNamespace
open WoofWare.Myriad.Plugins
/// Mock record type for an interface /// Mock record type for an interface
type internal PublicTypeMock = type internal PublicTypeMock =
{ {
@@ -13,6 +15,7 @@ type internal PublicTypeMock =
Mem3 : int * option<System.Threading.CancellationToken> -> string Mem3 : int * option<System.Threading.CancellationToken> -> string
} }
/// An implementation where every method throws.
static member Empty : PublicTypeMock = static member Empty : PublicTypeMock =
{ {
Mem1 = (fun x -> raise (System.NotImplementedException "Unimplemented mock function")) Mem1 = (fun x -> raise (System.NotImplementedException "Unimplemented mock function"))
@@ -26,6 +29,32 @@ type internal PublicTypeMock =
member this.Mem3 (arg_0_0, arg_0_1) = this.Mem3 (arg_0_0, arg_0_1) member this.Mem3 (arg_0_0, arg_0_1) = this.Mem3 (arg_0_0, arg_0_1)
namespace SomeNamespace namespace SomeNamespace
open WoofWare.Myriad.Plugins
/// Mock record type for an interface
type public PublicTypeInternalFalseMock =
{
Mem1 : string * int -> string list
Mem2 : string -> int
Mem3 : int * option<System.Threading.CancellationToken> -> string
}
/// An implementation where every method throws.
static member Empty : PublicTypeInternalFalseMock =
{
Mem1 = (fun x -> raise (System.NotImplementedException "Unimplemented mock function"))
Mem2 = (fun x -> raise (System.NotImplementedException "Unimplemented mock function"))
Mem3 = (fun x -> raise (System.NotImplementedException "Unimplemented mock function"))
}
interface IPublicTypeInternalFalse with
member this.Mem1 (arg_0_0, arg_0_1) = this.Mem1 (arg_0_0, arg_0_1)
member this.Mem2 (arg_0_0) = this.Mem2 (arg_0_0)
member this.Mem3 (arg_0_0, arg_0_1) = this.Mem3 (arg_0_0, arg_0_1)
namespace SomeNamespace
open WoofWare.Myriad.Plugins
/// Mock record type for an interface /// Mock record type for an interface
type internal InternalTypeMock = type internal InternalTypeMock =
{ {
@@ -33,6 +62,7 @@ type internal InternalTypeMock =
Mem2 : string -> int Mem2 : string -> int
} }
/// An implementation where every method throws.
static member Empty : InternalTypeMock = static member Empty : InternalTypeMock =
{ {
Mem1 = (fun x -> raise (System.NotImplementedException "Unimplemented mock function")) Mem1 = (fun x -> raise (System.NotImplementedException "Unimplemented mock function"))
@@ -44,6 +74,8 @@ type internal InternalTypeMock =
member this.Mem2 (arg_0_0) = this.Mem2 (arg_0_0) member this.Mem2 (arg_0_0) = this.Mem2 (arg_0_0)
namespace SomeNamespace namespace SomeNamespace
open WoofWare.Myriad.Plugins
/// Mock record type for an interface /// Mock record type for an interface
type private PrivateTypeMock = type private PrivateTypeMock =
{ {
@@ -51,6 +83,7 @@ type private PrivateTypeMock =
Mem2 : string -> int Mem2 : string -> int
} }
/// An implementation where every method throws.
static member Empty : PrivateTypeMock = static member Empty : PrivateTypeMock =
{ {
Mem1 = (fun x -> raise (System.NotImplementedException "Unimplemented mock function")) Mem1 = (fun x -> raise (System.NotImplementedException "Unimplemented mock function"))
@@ -62,12 +95,36 @@ type private PrivateTypeMock =
member this.Mem2 (arg_0_0) = this.Mem2 (arg_0_0) member this.Mem2 (arg_0_0) = this.Mem2 (arg_0_0)
namespace SomeNamespace namespace SomeNamespace
open WoofWare.Myriad.Plugins
/// Mock record type for an interface
type private PrivateTypeInternalFalseMock =
{
Mem1 : string * int -> unit
Mem2 : string -> int
}
/// An implementation where every method throws.
static member Empty : PrivateTypeInternalFalseMock =
{
Mem1 = (fun x -> raise (System.NotImplementedException "Unimplemented mock function"))
Mem2 = (fun x -> raise (System.NotImplementedException "Unimplemented mock function"))
}
interface PrivateTypeInternalFalse with
member this.Mem1 (arg_0_0, arg_0_1) = this.Mem1 (arg_0_0, arg_0_1)
member this.Mem2 (arg_0_0) = this.Mem2 (arg_0_0)
namespace SomeNamespace
open WoofWare.Myriad.Plugins
/// Mock record type for an interface /// Mock record type for an interface
type internal VeryPublicTypeMock<'a, 'b> = type internal VeryPublicTypeMock<'a, 'b> =
{ {
Mem1 : 'a -> 'b Mem1 : 'a -> 'b
} }
/// An implementation where every method throws.
static member Empty () : VeryPublicTypeMock<'a, 'b> = static member Empty () : VeryPublicTypeMock<'a, 'b> =
{ {
Mem1 = (fun x -> raise (System.NotImplementedException "Unimplemented mock function")) Mem1 = (fun x -> raise (System.NotImplementedException "Unimplemented mock function"))
@@ -77,6 +134,8 @@ type internal VeryPublicTypeMock<'a, 'b> =
member this.Mem1 (arg_0_0) = this.Mem1 (arg_0_0) member this.Mem1 (arg_0_0) = this.Mem1 (arg_0_0)
namespace SomeNamespace namespace SomeNamespace
open WoofWare.Myriad.Plugins
/// Mock record type for an interface /// Mock record type for an interface
type internal CurriedMock<'a> = type internal CurriedMock<'a> =
{ {
@@ -88,6 +147,7 @@ type internal CurriedMock<'a> =
Mem6 : int * string -> 'a * int -> string Mem6 : int * string -> 'a * int -> string
} }
/// An implementation where every method throws.
static member Empty () : CurriedMock<'a> = static member Empty () : CurriedMock<'a> =
{ {
Mem1 = (fun x -> raise (System.NotImplementedException "Unimplemented mock function")) Mem1 = (fun x -> raise (System.NotImplementedException "Unimplemented mock function"))

View File

@@ -8,6 +8,12 @@ type IPublicType =
abstract Mem2 : string -> int abstract Mem2 : string -> int
abstract Mem3 : x : int * ?ct : System.Threading.CancellationToken -> string abstract Mem3 : x : int * ?ct : System.Threading.CancellationToken -> string
[<GenerateMock false>]
type IPublicTypeInternalFalse =
abstract Mem1 : string * int -> string list
abstract Mem2 : string -> int
abstract Mem3 : x : int * ?ct : System.Threading.CancellationToken -> string
[<GenerateMock>] [<GenerateMock>]
type internal InternalType = type internal InternalType =
abstract Mem1 : string * int -> unit abstract Mem1 : string * int -> unit
@@ -18,6 +24,11 @@ type private PrivateType =
abstract Mem1 : string * int -> unit abstract Mem1 : string * int -> unit
abstract Mem2 : string -> int abstract Mem2 : string -> int
[<GenerateMock false>]
type private PrivateTypeInternalFalse =
abstract Mem1 : string * int -> unit
abstract Mem2 : string -> int
[<GenerateMock>] [<GenerateMock>]
type VeryPublicType<'a, 'b> = type VeryPublicType<'a, 'b> =
abstract Mem1 : 'a -> 'b abstract Mem1 : 'a -> 'b

View File

@@ -13,8 +13,14 @@ type RemoveOptionsAttribute () =
/// This generator creates a record which implements the interface, /// This generator creates a record which implements the interface,
/// but where each method is represented as a record field, so you can use /// but where each method is represented as a record field, so you can use
/// record update syntax to easily specify partially-implemented mock objects. /// record update syntax to easily specify partially-implemented mock objects.
type GenerateMockAttribute () = /// You may optionally specify `isInternal = false` to get a mock with the public visibility modifier.
type GenerateMockAttribute (isInternal : bool) =
inherit Attribute () inherit Attribute ()
/// The default value of `isInternal`, the optional argument to the GenerateMockAttribute constructor.
static member DefaultIsInternal = true
/// Shorthand for the "isExtensionMethod = false" constructor; see documentation there for details.
new () = GenerateMockAttribute GenerateMockAttribute.DefaultIsInternal
/// Attribute indicating a record type to which the "Add JSON serializer" Myriad /// Attribute indicating a record type to which the "Add JSON serializer" Myriad
/// generator should apply during build. /// generator should apply during build.

View File

@@ -1,5 +1,8 @@
WoofWare.Myriad.Plugins.GenerateMockAttribute inherit System.Attribute WoofWare.Myriad.Plugins.GenerateMockAttribute inherit System.Attribute
WoofWare.Myriad.Plugins.GenerateMockAttribute..ctor [constructor]: bool
WoofWare.Myriad.Plugins.GenerateMockAttribute..ctor [constructor]: unit WoofWare.Myriad.Plugins.GenerateMockAttribute..ctor [constructor]: unit
WoofWare.Myriad.Plugins.GenerateMockAttribute.DefaultIsInternal [static property]: [read-only] bool
WoofWare.Myriad.Plugins.GenerateMockAttribute.get_DefaultIsInternal [static method]: unit -> bool
WoofWare.Myriad.Plugins.HttpClientAttribute inherit System.Attribute WoofWare.Myriad.Plugins.HttpClientAttribute inherit System.Attribute
WoofWare.Myriad.Plugins.HttpClientAttribute..ctor [constructor]: unit WoofWare.Myriad.Plugins.HttpClientAttribute..ctor [constructor]: unit
WoofWare.Myriad.Plugins.JsonParseAttribute inherit System.Attribute WoofWare.Myriad.Plugins.JsonParseAttribute inherit System.Attribute

View File

@@ -1,5 +1,5 @@
{ {
"version": "2.0", "version": "2.1",
"publicReleaseRefSpec": [ "publicReleaseRefSpec": [
"^refs/heads/main$" "^refs/heads/main$"
], ],

View File

@@ -6,6 +6,11 @@ open Fantomas.FCS.SyntaxTrivia
open Fantomas.FCS.Xml open Fantomas.FCS.Xml
open Myriad.Core open Myriad.Core
type internal GenerateMockOutputSpec =
{
IsInternal : bool
}
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module internal InterfaceMockGenerator = module internal InterfaceMockGenerator =
open Fantomas.FCS.Text.Range open Fantomas.FCS.Text.Range
@@ -17,6 +22,7 @@ module internal InterfaceMockGenerator =
| Some id -> id | Some id -> id
let createType let createType
(spec : GenerateMockOutputSpec)
(name : string) (name : string)
(interfaceType : InterfaceType) (interfaceType : InterfaceType)
(xmlDoc : PreXmlDoc) (xmlDoc : PreXmlDoc)
@@ -92,7 +98,7 @@ module internal InterfaceMockGenerator =
false, false,
false, false,
[], [],
PreXmlDoc.Empty, PreXmlDoc.Create " An implementation where every method throws.",
SynValData.SynValData (Some synValData, SynValInfo.Empty, None), SynValData.SynValData (Some synValData, SynValInfo.Empty, None),
constructorIdent, constructorIdent,
Some constructorReturnType, Some constructorReturnType,
@@ -249,13 +255,14 @@ module internal InterfaceMockGenerator =
SynMemberDefn.Interface (interfaceName, Some range0, Some members, range0) SynMemberDefn.Interface (interfaceName, Some range0, Some members, range0)
// TODO: allow an arg to the attribute, specifying a custom visibility
let access = let access =
match interfaceType.Accessibility with match interfaceType.Accessibility, spec.IsInternal with
| Some (SynAccess.Public _) | Some (SynAccess.Public _), true
| Some (SynAccess.Internal _) | None, true -> SynAccess.Internal range0
| None -> SynAccess.Internal range0 | Some (SynAccess.Public _), false -> SynAccess.Public range0
| Some (SynAccess.Private _) -> SynAccess.Private range0 | None, false -> SynAccess.Public range0
| Some (SynAccess.Internal _), _ -> SynAccess.Internal range0
| Some (SynAccess.Private _), _ -> SynAccess.Private range0
let record = let record =
{ {
@@ -304,14 +311,19 @@ module internal InterfaceMockGenerator =
SynFieldTrivia.Zero SynFieldTrivia.Zero
) )
let createRecord (namespaceId : LongIdent) (interfaceType : SynTypeDefn) : SynModuleOrNamespace = let createRecord
(namespaceId : LongIdent)
(opens : SynOpenDeclTarget list)
(interfaceType : SynTypeDefn, spec : GenerateMockOutputSpec)
: SynModuleOrNamespace
=
let interfaceType = AstHelper.parseInterface interfaceType let interfaceType = AstHelper.parseInterface interfaceType
let fields = interfaceType.Members |> List.map constructMember let fields = interfaceType.Members |> List.map constructMember
let docString = PreXmlDoc.Create " Mock record type for an interface" let docString = PreXmlDoc.Create " Mock record type for an interface"
let name = let name =
List.last interfaceType.Name List.last interfaceType.Name
|> fun s -> s.idText |> _.idText
|> fun s -> |> fun s ->
if s.StartsWith 'I' && s.Length > 1 && Char.IsUpper s.[1] then if s.StartsWith 'I' && s.Length > 1 && Char.IsUpper s.[1] then
s.[1..] s.[1..]
@@ -319,9 +331,13 @@ module internal InterfaceMockGenerator =
s s
|> fun s -> s + "Mock" |> fun s -> s + "Mock"
let typeDecl = createType name interfaceType docString fields let typeDecl = createType spec name interfaceType docString fields
SynModuleOrNamespace.CreateNamespace (namespaceId, decls = [ typeDecl ])
SynModuleOrNamespace.CreateNamespace (
namespaceId,
decls = (opens |> List.map SynModuleDecl.CreateOpen) @ [ typeDecl ]
)
/// Myriad generator that creates a record which implements the given interface, /// Myriad generator that creates a record which implements the given interface,
/// but with every field mocked out. /// but with every field mocked out.
@@ -340,15 +356,37 @@ type InterfaceMockGenerator () =
let namespaceAndInterfaces = let namespaceAndInterfaces =
types types
|> List.choose (fun (ns, types) -> |> List.choose (fun (ns, types) ->
match types |> List.filter Ast.hasAttribute<GenerateMockAttribute> with types
| [] -> None |> List.choose (fun typeDef ->
| types -> Some (ns, types) match Ast.getAttribute<GenerateMockAttribute> typeDef with
| None -> None
| Some attr ->
let arg =
match SynExpr.stripOptionalParen attr.ArgExpr with
| SynExpr.Const (SynConst.Bool value, _) -> value
| SynExpr.Const (SynConst.Unit, _) -> GenerateMockAttribute.DefaultIsInternal
| arg ->
failwith
$"Unrecognised argument %+A{arg} to [<%s{nameof GenerateMockAttribute}>]. Literals are not supported. Use `true` or `false` (or unit) only."
let spec =
{
IsInternal = arg
}
Some (typeDef, spec)
)
|> function
| [] -> None
| ty -> Some (ns, ty)
) )
let opens = AstHelper.extractOpens ast let opens = AstHelper.extractOpens ast
let modules = let modules =
namespaceAndInterfaces namespaceAndInterfaces
|> List.collect (fun (ns, records) -> records |> List.map (InterfaceMockGenerator.createRecord ns)) |> List.collect (fun (ns, records) ->
records |> List.map (InterfaceMockGenerator.createRecord ns opens)
)
Output.Ast modules Output.Ast modules