11using System ;
22using System . IO ;
3+ using System . Threading ;
34using NUnit . Framework ;
4- using SIL . FieldWorks . FwCoreDlgs ;
55using SIL . FieldWorks . Common . FwUtils ;
6+ using SIL . FieldWorks . FwCoreDlgs ;
67using SIL . LCModel ;
78using SIL . LCModel . Core . KernelInterfaces ;
89using SIL . LCModel . Infrastructure ;
@@ -19,66 +20,91 @@ namespace SIL.FieldWorks.Common.RootSites.RootSiteTests
1920 public abstract class RealDataTestsBase
2021 {
2122 private const string ReusableProjectName = "integration_test_data" ;
23+ private const string ProjectMutexName = @"Local\FieldWorks.RealDataTests.integration_test_data" ;
24+ private const string TestProjectSentinelFileName = ".fieldworks-real-data-test-project" ;
25+ private const int DeleteRetryCount = 3 ;
26+ private static readonly TimeSpan DeleteRetryDelay = TimeSpan . FromMilliseconds ( 250 ) ;
2227
2328 protected FwNewLangProjectModel m_model ;
2429 protected LcmCache Cache ;
2530 protected string m_dbName ;
2631 private string m_projectDirectory ;
32+ private Mutex m_projectMutex ;
2733
2834 [ SetUp ]
2935 public virtual void TestSetup ( )
3036 {
3137 m_dbName = ReusableProjectName ;
3238 m_projectDirectory = DbDirectory ( m_dbName ) ;
33- DeleteProjectDirectory ( m_projectDirectory ) ;
39+ AcquireProjectMutex ( ) ;
3440
35- // Init New Lang Project Model (headless)
36- m_model = new FwNewLangProjectModel ( true )
37- {
38- LoadProjectNameSetup = ( ) => { } ,
39- LoadVernacularSetup = ( ) => { } ,
40- LoadAnalysisSetup = ( ) => { } ,
41- AnthroModel = new FwChooseAnthroListModel { CurrentList = FwChooseAnthroListModel . ListChoice . UserDef }
42- } ;
43-
44- string createdPath ;
45- using ( var threadHelper = new ThreadHelper ( ) )
41+ try
4642 {
47- m_model . ProjectName = m_dbName ;
48- m_model . Next ( ) ; // To Vernacular WS Setup
49- m_model . SetDefaultWs ( new LanguageInfo { LanguageTag = "qaa" , DesiredName = "Vernacular" } ) ;
50- m_model . Next ( ) ; // To Analysis WS Setup
51- m_model . SetDefaultWs ( new LanguageInfo { LanguageTag = "en" , DesiredName = "English" } ) ;
52- createdPath = m_model . CreateNewLangProj ( new DummyProgressDlg ( ) , threadHelper ) ;
53- m_projectDirectory = Path . GetDirectoryName ( createdPath ) ;
54- }
43+ DeleteProjectDirectory ( m_projectDirectory ) ;
44+
45+ m_model = new FwNewLangProjectModel ( true )
46+ {
47+ LoadProjectNameSetup = ( ) => { } ,
48+ LoadVernacularSetup = ( ) => { } ,
49+ LoadAnalysisSetup = ( ) => { } ,
50+ AnthroModel = new FwChooseAnthroListModel
51+ {
52+ CurrentList = FwChooseAnthroListModel . ListChoice . UserDef ,
53+ } ,
54+ } ;
55+
56+ string createdPath ;
57+ using ( var threadHelper = new ThreadHelper ( ) )
58+ {
59+ m_model . ProjectName = m_dbName ;
60+ m_model . Next ( ) ; // To Vernacular WS Setup
61+ m_model . SetDefaultWs (
62+ new LanguageInfo { LanguageTag = "qaa" , DesiredName = "Vernacular" }
63+ ) ;
64+ m_model . Next ( ) ; // To Analysis WS Setup
65+ m_model . SetDefaultWs (
66+ new LanguageInfo { LanguageTag = "en" , DesiredName = "English" }
67+ ) ;
68+ createdPath = m_model . CreateNewLangProj ( new DummyProgressDlg ( ) , threadHelper ) ;
69+ m_projectDirectory = GetProjectDirectory ( createdPath ) ;
70+ WriteTestProjectSentinel ( m_projectDirectory ) ;
71+ }
5572
56- // Load the cache from the newly created .fwdata file
57- Cache = LcmCache . CreateCacheFromExistingData (
58- new TestProjectId ( BackendProviderType . kXMLWithMemoryOnlyWsMgr , createdPath ) ,
59- "en" ,
60- new DummyLcmUI ( ) ,
61- FwDirectoryFinder . LcmDirectories ,
62- new LcmSettings ( ) ,
63- new DummyProgressDlg ( ) ) ;
73+ Cache = LcmCache . CreateCacheFromExistingData (
74+ new TestProjectId ( BackendProviderType . kXMLWithMemoryOnlyWsMgr , createdPath ) ,
75+ "en" ,
76+ new DummyLcmUI ( ) ,
77+ FwDirectoryFinder . LcmDirectories ,
78+ new LcmSettings ( ) ,
79+ new DummyProgressDlg ( )
80+ ) ;
6481
65- try
66- {
67- using ( var undoWatcher = new UndoableUnitOfWorkHelper ( Cache . ActionHandlerAccessor , "Test Setup" , "Undo Test Setup" ) )
82+ try
6883 {
69- InitializeProjectData ( ) ;
70- CreateTestData ( ) ;
71- undoWatcher . RollBack = false ;
84+ using (
85+ var undoWatcher = new UndoableUnitOfWorkHelper (
86+ Cache . ActionHandlerAccessor ,
87+ "Test Setup" ,
88+ "Undo Test Setup"
89+ )
90+ )
91+ {
92+ InitializeProjectData ( ) ;
93+ CreateTestData ( ) ;
94+ undoWatcher . RollBack = false ;
95+ }
96+ }
97+ catch ( Exception )
98+ {
99+ DisposeCache ( ) ;
100+ throw ;
72101 }
73102 }
74103 catch ( Exception )
75104 {
76- // If setup fails, ensure we don't leave a locked DB
77- if ( Cache != null )
78- {
79- Cache . Dispose ( ) ;
80- Cache = null ;
81- }
105+ DisposeCache ( ) ;
106+ TryDeleteProjectDirectoryAfterSetupFailure ( ) ;
107+ ReleaseProjectMutex ( ) ;
82108 throw ;
83109 }
84110 }
@@ -96,27 +122,201 @@ protected virtual void CreateTestData()
96122 [ TearDown ]
97123 public virtual void TestTearDown ( )
98124 {
99- if ( Cache != null )
125+ try
100126 {
101- Cache . Dispose ( ) ;
102- Cache = null ;
127+ DisposeCache ( ) ;
128+ DeleteProjectDirectory ( m_projectDirectory ) ;
129+ }
130+ finally
131+ {
132+ m_projectDirectory = null ;
133+ ReleaseProjectMutex ( ) ;
103134 }
104-
105- DeleteProjectDirectory ( m_projectDirectory ) ;
106- m_projectDirectory = null ;
107135 }
108136
109137 protected string DbDirectory ( string name )
110138 {
111139 return Path . Combine ( FwDirectoryFinder . ProjectsDirectory , name ) ;
112140 }
113141
142+ private void AcquireProjectMutex ( )
143+ {
144+ m_projectMutex = new Mutex ( false , ProjectMutexName ) ;
145+ try
146+ {
147+ m_projectMutex . WaitOne ( ) ;
148+ }
149+ catch ( AbandonedMutexException )
150+ {
151+ }
152+ }
153+
154+ private void ReleaseProjectMutex ( )
155+ {
156+ if ( m_projectMutex == null )
157+ return ;
158+
159+ try
160+ {
161+ m_projectMutex . ReleaseMutex ( ) ;
162+ }
163+ catch ( ApplicationException )
164+ {
165+ }
166+ finally
167+ {
168+ m_projectMutex . Dispose ( ) ;
169+ m_projectMutex = null ;
170+ }
171+ }
172+
173+ private void DisposeCache ( )
174+ {
175+ if ( Cache == null )
176+ return ;
177+
178+ Cache . Dispose ( ) ;
179+ Cache = null ;
180+ }
181+
182+ private void TryDeleteProjectDirectoryAfterSetupFailure ( )
183+ {
184+ try
185+ {
186+ DeleteProjectDirectory ( m_projectDirectory ) ;
187+ }
188+ catch ( Exception e )
189+ {
190+ TestContext . Error . WriteLine (
191+ "Could not clean up test project directory '{0}' after setup failure: {1}" ,
192+ m_projectDirectory ,
193+ e . Message
194+ ) ;
195+ }
196+ }
197+
198+ private static string GetProjectDirectory ( string createdPath )
199+ {
200+ if ( string . IsNullOrEmpty ( createdPath ) )
201+ throw new InvalidOperationException ( "CreateNewLangProj did not return a project path." ) ;
202+
203+ var fullPath = NormalizePath ( createdPath ) ;
204+ if ( Directory . Exists ( fullPath ) )
205+ {
206+ EnsureSafeProjectDirectory ( fullPath ) ;
207+ return fullPath ;
208+ }
209+
210+ if ( ! File . Exists ( fullPath ) )
211+ throw new FileNotFoundException ( "CreateNewLangProj returned a path that does not exist." , fullPath ) ;
212+
213+ var projectDirectory = Path . GetDirectoryName ( fullPath ) ;
214+ EnsureSafeProjectDirectory ( projectDirectory ) ;
215+ return projectDirectory ;
216+ }
217+
218+ private static void WriteTestProjectSentinel ( string projectDirectory )
219+ {
220+ EnsureSafeProjectDirectory ( projectDirectory ) ;
221+ File . WriteAllText (
222+ GetSentinelFilePath ( projectDirectory ) ,
223+ "Created by FieldWorks RootSiteTests. This directory is safe for tests to delete."
224+ ) ;
225+ }
226+
114227 private static void DeleteProjectDirectory ( string projectDirectory )
115228 {
116229 if ( string . IsNullOrEmpty ( projectDirectory ) || ! Directory . Exists ( projectDirectory ) )
117230 return ;
118231
119- try { Directory . Delete ( projectDirectory , true ) ; } catch { }
232+ var safeProjectDirectory = NormalizePath ( projectDirectory ) ;
233+ EnsureSafeProjectDirectory ( safeProjectDirectory ) ;
234+
235+ if ( ! File . Exists ( GetSentinelFilePath ( safeProjectDirectory ) ) )
236+ {
237+ throw new InvalidOperationException (
238+ string . Format (
239+ "Refusing to delete '{0}' because the test sentinel file '{1}' is missing." ,
240+ safeProjectDirectory ,
241+ TestProjectSentinelFileName
242+ )
243+ ) ;
244+ }
245+
246+ Exception lastException = null ;
247+ for ( var attempt = 1 ; attempt <= DeleteRetryCount ; attempt ++ )
248+ {
249+ try
250+ {
251+ Directory . Delete ( safeProjectDirectory , true ) ;
252+ return ;
253+ }
254+ catch ( IOException e )
255+ {
256+ lastException = e ;
257+ LogDeleteFailure ( safeProjectDirectory , attempt , e ) ;
258+ }
259+ catch ( UnauthorizedAccessException e )
260+ {
261+ lastException = e ;
262+ LogDeleteFailure ( safeProjectDirectory , attempt , e ) ;
263+ }
264+
265+ if ( attempt < DeleteRetryCount )
266+ Thread . Sleep ( DeleteRetryDelay ) ;
267+ }
268+
269+ throw new IOException (
270+ string . Format (
271+ "Could not delete test project directory '{0}' after {1} attempts." ,
272+ safeProjectDirectory ,
273+ DeleteRetryCount
274+ ) ,
275+ lastException
276+ ) ;
277+ }
278+
279+ private static void LogDeleteFailure ( string projectDirectory , int attempt , Exception e )
280+ {
281+ TestContext . Error . WriteLine (
282+ "Could not delete test project directory '{0}' on attempt {1} of {2}: {3}" ,
283+ projectDirectory ,
284+ attempt ,
285+ DeleteRetryCount ,
286+ e . Message
287+ ) ;
288+ }
289+
290+ private static void EnsureSafeProjectDirectory ( string projectDirectory )
291+ {
292+ if ( string . IsNullOrEmpty ( projectDirectory ) )
293+ throw new InvalidOperationException ( "The test project directory path is empty." ) ;
294+
295+ var safeProjectDirectory = NormalizePath ( projectDirectory ) ;
296+ var expectedProjectDirectory = NormalizePath (
297+ Path . Combine ( FwDirectoryFinder . ProjectsDirectory , ReusableProjectName )
298+ ) ;
299+
300+ if ( ! string . Equals ( safeProjectDirectory , expectedProjectDirectory , StringComparison . OrdinalIgnoreCase ) )
301+ {
302+ throw new InvalidOperationException (
303+ string . Format (
304+ "Refusing to use test project directory '{0}'; expected '{1}'." ,
305+ safeProjectDirectory ,
306+ expectedProjectDirectory
307+ )
308+ ) ;
309+ }
310+ }
311+
312+ private static string GetSentinelFilePath ( string projectDirectory )
313+ {
314+ return Path . Combine ( projectDirectory , TestProjectSentinelFileName ) ;
315+ }
316+
317+ private static string NormalizePath ( string path )
318+ {
319+ return Path . GetFullPath ( path ) . TrimEnd ( Path . DirectorySeparatorChar , Path . AltDirectorySeparatorChar ) ;
120320 }
121321 }
122322}
0 commit comments