22 * Copyright (c) Microsoft Corporation. All rights reserved.
33 *--------------------------------------------------------------------------------------------*/
44
5+ using System . Collections ;
56using System . Reflection ;
7+ using System . Runtime . CompilerServices ;
68using System . Runtime . InteropServices ;
79using Xunit . Sdk ;
810
911namespace GitHub . Copilot . Test . Harness ;
1012
11- // =============================================================================
12- // TEMPORARY in-process env-var isolation. DELETE THIS ENTIRE FILE (and its two
13- // references in E2ETestContext plus the [assembly: InProcessEnvIsolation] in
14- // AssemblyInfo.cs) once the runtime stops reading the ambient process
15- // environment host-side.
16- //
17- // Why this exists
18- // ---------------
19- // Over the in-process FFI transport several runtime code paths run host-side in
20- // THIS process (the loaded cdylib) and read the ambient process environment
21- // rather than the environment passed to copilot_runtime_host_start — e.g.
22- // native fetch_copilot_user reads COPILOT_DEBUG_GITHUB_API_URL via
23- // std::env::var, the gh-CLI fallback spawns `gh auth token` (inheriting this
24- // process's GH_TOKEN / GITHUB_TOKEN / GH_CONFIG_DIR), auth-method selection
25- // reads the HMAC keys, and session state/config reads COPILOT_HOME / XDG_*.
26- // So the per-test environment we hand to CopilotClient is invisible to them.
27- //
28- // Two problems follow, both handled here:
29- // 1. CreateClient-routed tests must mirror their per-test environment onto this
30- // process so host-side reads observe the replay proxy, isolated home, and
31- // cleared credentials. See Mirror/Restore below.
32- // 2. Tests that construct CopilotClient directly (e.g. ClientE2ETests) never go
33- // through CreateClient, so nothing clears the ambient CI HMAC credential;
34- // in the in-process job they would authenticate for real and hit the live
35- // api.githubcopilot.com. The assembly-level BeforeAfterTest attribute below
36- // neutralizes those ambient credentials around EVERY test, independent of
37- // how the client is constructed.
38- //
39- // Everything here is gated to the in-process job (COPILOT_SDK_DEFAULT_CONNECTION
40- // == "inprocess") and is a no-op otherwise.
41- // =============================================================================
42-
43- /// <summary>
44- /// Owns all process-wide environment mutation used to make the in-process FFI
45- /// transport hermetic in tests. Consolidated in one file so it can be deleted
46- /// wholesale once the runtime no longer reads the ambient process environment.
47- /// </summary>
13+ // Because many of the tests mutate global environment variables, we have to snapshot the original
14+ // state and restore it after each test. Otherwise tests influence each other depending on run order.
15+ // This is especially important for the in-process transport because the runtime is inside the test
16+ // host process and will be reading/writing its environment variables directly.
4817internal static class InProcessEnvIsolation
4918{
50- // Ambient credentials that would otherwise let a directly-constructed client
51- // authenticate for real (and reach the live API) in the in-process job. CI
52- // sets COPILOT_HMAC_KEY at the job level; the replay snapshots are captured
53- // against Bearer/OAuth requests, so real HMAC auth must be disabled. An empty
54- // value disables the method (the runtime filters out empty HMAC keys).
55- private static readonly string [ ] LeakyCredentialVars = [ "COPILOT_HMAC_KEY" , "CAPI_HMAC_KEY" ] ;
19+ // Unset because CI sets them but the replay snapshots expect Bearer/OAuth.
20+ private static readonly string [ ] SuppressEnvVars = [ "COPILOT_HMAC_KEY" , "CAPI_HMAC_KEY" ] ;
5621
57- private static readonly object s_lock = new ( ) ;
22+ // Captured at load, before any fixture/test mutates env.
23+ private static readonly Dictionary < string , string ? > s_ambient = CaptureEnvironment ( ) ;
5824
59- // Process-wide, permanent record of the PRISTINE (pre-any-mirror) value of
60- // every variable we have ever overwritten. A null entry means the variable
61- // was originally unset. Captured once per name and never overwritten, so it
62- // is immune to a cascade in which a skipped restore would otherwise let a
63- // later test back up an already-polluted value as its baseline. Static
64- // because the shared process env is itself process-global and E2E tests run
65- // serially (DisableTestParallelization).
66- private static readonly Dictionary < string , string ? > s_pristineByName = new ( ) ;
25+ // Runs at assembly load so the ambient env is snapshotted before the shared
26+ // fixture mirrors per-test env onto the process. Justifies suppressing CA2255.
27+ #pragma warning disable CA2255 // ModuleInitializer discouraged in libraries; intentional in this test harness.
28+ [ ModuleInitializer ]
29+ internal static void CaptureAtLoad ( ) => _ = s_ambient ;
30+ #pragma warning restore CA2255
6731
6832 [ DllImport ( "libc" , EntryPoint = "setenv" , CharSet = CharSet . Ansi ,
6933 BestFitMapping = false , ThrowOnUnmappableChar = true ) ]
@@ -73,124 +37,57 @@ internal static class InProcessEnvIsolation
7337 BestFitMapping = false , ThrowOnUnmappableChar = true ) ]
7438 private static extern int NativeUnsetEnv ( string name ) ;
7539
76- /// <summary>
77- /// Whether the in-process FFI transport is the default for this run, honoring
78- /// COPILOT_SDK_DEFAULT_CONNECTION from the supplied per-test environment (if
79- /// any) else the process environment. Mirrors CopilotClient's own resolution.
80- /// </summary>
81- public static bool IsActive ( IReadOnlyDictionary < string , string > ? environment = null )
82- {
83- var value = environment is not null
84- && environment . TryGetValue ( "COPILOT_SDK_DEFAULT_CONNECTION" , out var fromOptions )
85- ? fromOptions
86- : Environment . GetEnvironmentVariable ( "COPILOT_SDK_DEFAULT_CONNECTION" ) ;
87- return string . Equals ( value , "inprocess" , StringComparison . OrdinalIgnoreCase ) ;
88- }
89-
90- /// <summary>
91- /// Mirrors a variable onto the shared process environment for the in-process
92- /// host-side runtime to observe, recording the pristine value once so
93- /// <see cref="Restore"/> can always revert to the true original.
94- /// </summary>
95- public static void Mirror ( string name , string value )
40+ // Sets/unsets on the managed cache and, on Unix, the libc block so native
41+ // readers in the loaded cdylib observe it.
42+ public static void Apply ( string name , string ? value )
9643 {
97- lock ( s_lock )
98- {
99- if ( ! s_pristineByName . ContainsKey ( name ) )
100- {
101- s_pristineByName [ name ] = Environment . GetEnvironmentVariable ( name ) ;
102- }
103- }
104-
105- SetProcessEnvironmentVariable ( name , value ) ;
106- }
107-
108- /// <summary>
109- /// Reverts every variable ever touched by <see cref="Mirror"/> back to its
110- /// permanently-recorded pristine value (or unsets it). Idempotent and
111- /// cascade-proof: because pristine values are never overwritten, calling this
112- /// always restores the true ambient environment even if a previous restore
113- /// was skipped.
114- /// </summary>
115- public static void Restore ( )
116- {
117- KeyValuePair < string , string ? > [ ] pristine ;
118- lock ( s_lock )
119- {
120- if ( s_pristineByName . Count == 0 )
121- {
122- return ;
123- }
124-
125- pristine = [ .. s_pristineByName ] ;
126- }
127-
128- foreach ( var ( name , value ) in pristine )
44+ Environment . SetEnvironmentVariable ( name , value ) ;
45+ if ( ! RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
12946 {
130- RestoreProcessEnvironmentVariable ( name , value ) ;
47+ _ = value is null ? NativeUnsetEnv ( name ) : NativeSetEnv ( name , value , 1 ) ;
13148 }
13249 }
13350
134- /// <summary>
135- /// Neutralizes ambient credentials that would otherwise let a directly
136- /// constructed client authenticate for real in the in-process job. Recorded
137- /// via <see cref="Mirror"/> so <see cref="Restore"/> reverts them.
138- /// </summary>
13951 public static void NeutralizeAmbientCredentials ( )
14052 {
141- foreach ( var name in LeakyCredentialVars )
53+ foreach ( var name in SuppressEnvVars )
14254 {
143- Mirror ( name , "" ) ;
55+ Apply ( name , null ) ;
14456 }
14557 }
14658
147- // Sets an environment variable on both the managed cache and (on Unix) the
148- // libc environment block, so native getenv/std::env::var readers in the
149- // loaded cdylib observe it. On Windows the managed setter already reaches
150- // native GetEnvironmentVariableW, so setenv is not needed.
151- private static void SetProcessEnvironmentVariable ( string name , string value )
59+ public static void RestoreAmbient ( )
15260 {
153- Environment . SetEnvironmentVariable ( name , value ) ;
154- if ( ! RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
61+ foreach ( DictionaryEntry entry in Environment . GetEnvironmentVariables ( ) )
15562 {
156- _ = NativeSetEnv ( name , value , 1 ) ;
63+ var name = ( string ) entry . Key ;
64+ if ( ! s_ambient . ContainsKey ( name ) )
65+ {
66+ Apply ( name , null ) ;
67+ }
15768 }
158- }
15969
160- // Restores (or unsets) an environment variable on both the managed cache and
161- // (on Unix) the libc environment block.
162- private static void RestoreProcessEnvironmentVariable ( string name , string ? value )
163- {
164- Environment . SetEnvironmentVariable ( name , value ) ;
165- if ( ! RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
70+ foreach ( var ( name , value ) in s_ambient )
16671 {
167- _ = value is null ? NativeUnsetEnv ( name ) : NativeSetEnv ( name , value , 1 ) ;
72+ if ( ! string . Equals ( Environment . GetEnvironmentVariable ( name ) , value , StringComparison . Ordinal ) )
73+ {
74+ Apply ( name , value ) ;
75+ }
16876 }
16977 }
78+
79+ private static Dictionary < string , string ? > CaptureEnvironment ( ) =>
80+ Environment . GetEnvironmentVariables ( )
81+ . Cast < DictionaryEntry > ( )
82+ . ToDictionary ( e => ( string ) e . Key , e => e . Value ? . ToString ( ) , StringComparer . Ordinal ) ;
17083}
17184
172- /// <summary>
173- /// Assembly-level xUnit hook that isolates the ambient process environment around
174- /// every test in the in-process job, independent of how the CopilotClient is
175- /// constructed. No-op outside the in-process job. TEMPORARY — see
176- /// <see cref="InProcessEnvIsolation"/>.
177- /// </summary>
17885[ AttributeUsage ( AttributeTargets . Assembly , AllowMultiple = false ) ]
17986public sealed class InProcessEnvIsolationAttribute : BeforeAfterTestAttribute
18087{
181- public override void Before ( MethodInfo methodUnderTest )
182- {
183- if ( InProcessEnvIsolation . IsActive ( ) )
184- {
185- InProcessEnvIsolation . NeutralizeAmbientCredentials ( ) ;
186- }
187- }
88+ public override void Before ( MethodInfo methodUnderTest ) =>
89+ InProcessEnvIsolation . NeutralizeAmbientCredentials ( ) ;
18890
189- public override void After ( MethodInfo methodUnderTest )
190- {
191- if ( InProcessEnvIsolation . IsActive ( ) )
192- {
193- InProcessEnvIsolation . Restore ( ) ;
194- }
195- }
91+ public override void After ( MethodInfo methodUnderTest ) =>
92+ InProcessEnvIsolation . RestoreAmbient ( ) ;
19693}
0 commit comments