6
6
from importlib .resources import files
7
7
from io import BytesIO
8
8
from pathlib import Path
9
- from typing import TYPE_CHECKING , Any , Literal , ParamSpec , TextIO
9
+ from typing import TYPE_CHECKING , Literal , ParamSpec , Protocol
10
10
import asyncio
11
11
import json
12
12
import logging
51
51
)
52
52
53
53
if TYPE_CHECKING :
54
- from collections .abc import Callable , Iterable , Mapping
54
+ from collections .abc import Awaitable , Callable , Iterable , Mapping
55
+ from typing import Any , TextIO
55
56
56
57
from referencing .jsonschema import Schema , SchemaRegistry
57
58
@@ -172,6 +173,70 @@ def run(context: click.Context, *args: P.args, **kwargs: P.kwargs) -> None:
172
173
return run
173
174
174
175
176
+ class ImplementationSubcommand (Protocol ):
177
+ def __call__ (
178
+ self ,
179
+ implementations : Iterable [Implementation ],
180
+ ** kwargs : Any ,
181
+ ) -> Awaitable [int ]:
182
+ ...
183
+
184
+
185
+ def implementation_subcommand (fn : ImplementationSubcommand ):
186
+ """
187
+ Define a Bowtie subcommand which starts up some implementations.
188
+
189
+ Runs the wrapped function with only the successfully started
190
+ implementations.
191
+ """
192
+
193
+ async def run (image_names : list [str ], ** kwargs : Any ) -> int :
194
+ exit_code = 0
195
+ start = _start (
196
+ image_names = image_names ,
197
+ make_validator = validator_for_dialect ,
198
+ reporter = _report .Reporter (write = lambda ** _ : None ), # type: ignore[reportUnknownArgumentType]
199
+ )
200
+
201
+ running : list [Implementation ] = []
202
+ async with start as implementations :
203
+ for each in implementations :
204
+ try :
205
+ implementation = await each
206
+ except NoSuchImage as error :
207
+ exit_code |= _EX_CONFIG
208
+ click .echo ( # FIXME: respect a possible --quiet
209
+ f"❗ (error): { error .name !r} is not a "
210
+ "known Bowtie implementation." ,
211
+ file = sys .stderr ,
212
+ )
213
+ continue
214
+
215
+ try :
216
+ implementation .info ()
217
+ except StartupFailed :
218
+ exit_code |= _EX_CONFIG
219
+ click .echo ( # FIXME: respect a possible --quiet
220
+ f"❗ (error): { implementation .name } failed to start" ,
221
+ file = sys .stderr ,
222
+ )
223
+ continue
224
+
225
+ running .append (implementation )
226
+
227
+ exit_code |= await fn (implementations = running , ** kwargs )
228
+
229
+ return exit_code
230
+
231
+ @subcommand
232
+ @IMPLEMENTATION
233
+ @wraps (fn )
234
+ def cmd (image_names : list [str ], * args : P .args , ** kwargs : P .kwargs ) -> int :
235
+ return asyncio .run (run (* args , image_names = image_names , ** kwargs ))
236
+
237
+ return cmd
238
+
239
+
175
240
@subcommand
176
241
@click .option (
177
242
"--input" ,
@@ -627,7 +692,7 @@ async def _info(image_names: list[str], format: _F):
627
692
return exit_code
628
693
629
694
630
- @subcommand
695
+ @implementation_subcommand # type: ignore[reportArgumentType]
631
696
@click .option (
632
697
"-q" ,
633
698
"--quiet" ,
@@ -640,115 +705,75 @@ async def _info(image_names: list[str], format: _F):
640
705
help = "Don't print any output, just exit with nonzero status on failure." ,
641
706
)
642
707
@FORMAT
643
- @IMPLEMENTATION
644
- def smoke (** kwargs : Any ):
645
- """
646
- Smoke test one or more implementations for basic correctness.
647
- """
648
- return asyncio .run (_smoke (** kwargs ))
649
-
650
-
651
- async def _smoke (
652
- image_names : list [str ],
708
+ async def smoke (
709
+ implementations : Iterable [Implementation ],
653
710
format : _F ,
654
711
echo : Callable [..., None ],
655
- ):
656
- reporter = _report .Reporter (write = lambda ** _ : None ) # type: ignore[reportUnknownArgumentType]
712
+ ) -> int :
657
713
exit_code = 0
658
714
659
- match format :
660
- case "json" :
661
-
662
- def finish () -> None :
663
- echo (json .dumps (serializable , indent = 2 ))
664
-
665
- case "pretty" :
666
-
667
- def finish ():
668
- if exit_code :
669
- echo ("\n ❌ some failures" , file = sys .stderr )
670
- else :
671
- echo ("\n ✅ all passed" , file = sys .stderr )
672
-
673
- async with _start (
674
- image_names = image_names ,
675
- make_validator = validator_for_dialect ,
676
- reporter = reporter ,
677
- ) as starting :
678
- for each in starting :
679
- try :
680
- implementation = await each
681
- except NoSuchImage as error :
682
- exit_code |= _EX_CONFIG
683
- echo (
684
- f"❗ (error): { error .name !r} is not a known Bowtie implementation." ,
685
- file = sys .stderr ,
686
- )
687
- continue
715
+ for implementation in implementations :
716
+ echo (f"Testing { implementation .name !r} ...\n " , file = sys .stderr )
717
+
718
+ # FIXME: All dialects / and/or newest dialect with proper sort
719
+ dialect = max (implementation .info ().dialects , key = str )
720
+ runner = await implementation .start_speaking (dialect )
721
+
722
+ cases = [
723
+ TestCase (
724
+ description = "allow-everything schema" ,
725
+ schema = {"$schema" : str (dialect )},
726
+ tests = [
727
+ Test (description = "First" , instance = 1 , valid = True ),
728
+ Test (description = "Second" , instance = "foo" , valid = True ),
729
+ ],
730
+ ),
731
+ TestCase (
732
+ description = "allow-nothing schema" ,
733
+ schema = {"$schema" : str (dialect ), "not" : {}},
734
+ tests = [
735
+ Test (description = "First" , instance = 12 , valid = False ),
736
+ ],
737
+ ),
738
+ ]
688
739
689
- echo (f"Testing { implementation .name !r} ...\n " , file = sys .stderr )
740
+ match format :
741
+ case "json" :
742
+ serializable : list [dict [str , Any ]] = []
690
743
691
- try :
692
- implementation .info ()
693
- except StartupFailed :
694
- exit_code |= _EX_CONFIG
695
- click .echo (" ❗ (error): startup failed" )
696
- continue
744
+ def see (seq_case : SeqCase , result : SeqResult ): # type: ignore[reportRedeclaration]
745
+ serializable .append ( # noqa: B023
746
+ dict (
747
+ case = seq_case .case .without_expected_results (),
748
+ result = asdict (result .result ),
749
+ ),
750
+ )
697
751
698
- # FIXME: All dialects / and/or newest dialect with proper sort
699
- dialect = max (implementation .info ().dialects , key = str )
700
- runner = await implementation .start_speaking (dialect )
701
-
702
- cases = [
703
- TestCase (
704
- description = "allow-everything schema" ,
705
- schema = {"$schema" : str (dialect )},
706
- tests = [
707
- Test (description = "First" , instance = 1 , valid = True ),
708
- Test (description = "Second" , instance = "foo" , valid = True ),
709
- ],
710
- ),
711
- TestCase (
712
- description = "allow-nothing schema" ,
713
- schema = {"$schema" : str (dialect ), "not" : {}},
714
- tests = [
715
- Test (description = "First" , instance = 12 , valid = False ),
716
- ],
717
- ),
718
- ]
752
+ case "pretty" :
719
753
720
- match format :
721
- case "json" :
722
- serializable : list [dict [str , Any ]] = []
723
-
724
- def see (seq_case : SeqCase , result : SeqResult ): # type: ignore[reportRedeclaration]
725
- serializable .append ( # noqa: B023
726
- dict (
727
- case = seq_case .case .without_expected_results (),
728
- result = asdict (result .result ),
729
- ),
730
- )
754
+ def see (seq_case : SeqCase , response : SeqResult ):
755
+ signs = "" .join (
756
+ "❗" if succeeded is None else "✓" if succeeded else "✗"
757
+ for succeeded in response .compare ()
758
+ )
759
+ echo (f" · { seq_case .case .description } : { signs } " )
731
760
732
- case "pretty" :
761
+ for seq_case in SeqCase .for_cases (cases ):
762
+ result = await seq_case .run (runner = runner )
763
+ if result .unsuccessful ().causes_stop :
764
+ exit_code |= _EX_DATAERR
765
+ see (seq_case , result )
733
766
734
- def see (seq_case : SeqCase , response : SeqResult ):
735
- signs = "" .join (
736
- "❗"
737
- if succeeded is None
738
- else "✓"
739
- if succeeded
740
- else "✗"
741
- for succeeded in response .compare ()
742
- )
743
- echo (f" · { seq_case .case .description } : { signs } " )
767
+ match format :
768
+ case "json" :
769
+ echo (json .dumps (serializable , indent = 2 )) # type: ignore[reportPossiblyUnboundVariable]
744
770
745
- for seq_case in SeqCase . for_cases ( cases ) :
746
- result = await seq_case . run ( runner = runner )
747
- if result . unsuccessful (). causes_stop :
748
- exit_code |= _EX_DATAERR
749
- see ( seq_case , result )
771
+ case "pretty" :
772
+ if exit_code :
773
+ echo ( " \n ❌ some failures" , file = sys . stderr )
774
+ else :
775
+ echo ( " \n ✅ all passed" , file = sys . stderr )
750
776
751
- finish ()
752
777
return exit_code
753
778
754
779
0 commit comments