2121import static org .eclipse .core .tests .resources .ResourceTestUtil .createInWorkspace ;
2222import static org .eclipse .core .tests .resources .ResourceTestUtil .createTestMonitor ;
2323import static org .eclipse .core .tests .resources .ResourceTestUtil .waitForRefresh ;
24- import static org .junit .Assert .assertEquals ;
25- import static org .junit .Assert .assertTrue ;
26- import static org .junit .Assume .assumeTrue ;
24+ import static org .junit .jupiter . api . Assertions .assertEquals ;
25+ import static org .junit .jupiter . api . Assertions .assertTrue ;
26+ import static org .junit .jupiter . api . Assumptions .assumeTrue ;
2727
2828import java .io .IOException ;
29+ import java .nio .file .Files ;
30+ import java .nio .file .Path ;
31+ import java .nio .file .Paths ;
2932import org .eclipse .core .filesystem .EFS ;
3033import org .eclipse .core .filesystem .IFileStore ;
34+ import org .eclipse .core .internal .localstore .UnifiedTree ;
3135import org .eclipse .core .resources .IProject ;
3236import org .eclipse .core .resources .IResource ;
3337import org .eclipse .core .resources .IResourceVisitor ;
3438import org .eclipse .core .resources .IWorkspaceRunnable ;
3539import org .eclipse .core .runtime .CoreException ;
3640import org .eclipse .core .runtime .IPath ;
37- import org .eclipse .core .tests .resources .WorkspaceTestRule ;
38- import org .junit .Rule ;
39- import org .junit .Test ;
40-
41+ import org .eclipse .core .runtime .Platform .OS ;
42+ import org .eclipse .core .tests .resources .util .WorkspaceResetExtension ;
43+ import org .junit .jupiter .api .Test ;
44+ import org .junit .jupiter .api .extension .ExtendWith ;
45+ import org .junit .jupiter .params .ParameterizedTest ;
46+ import org .junit .jupiter .params .provider .ValueSource ;
47+
48+ @ ExtendWith (WorkspaceResetExtension .class )
4149public class SymlinkResourceTest {
4250
43- @ Rule
44- public WorkspaceTestRule workspaceRule = new WorkspaceTestRule ();
45-
4651 private void mkLink (IFileStore dir , String src , String tgt , boolean isDir ) throws CoreException , IOException {
4752 createSymLink (dir .toLocalFile (EFS .NONE , createTestMonitor ()), src , tgt , isDir );
4853 }
@@ -70,6 +75,88 @@ protected void createBug358830Structure(IFileStore rootDir) throws CoreException
7075 mkLink (folderA , "link" , IPath .fromOSString ("../" ).toOSString (), true );
7176 }
7277
78+ /**
79+ * Test a case of both recursive and non recursive symbolic links that uncovered
80+ * issue described in <a href=
81+ * "https://github.com/eclipse-platform/eclipse.platform/issues/2220">GitHub bug
82+ * 2220</a>.
83+ *
84+ * <pre>{@code
85+ * /A/B -> /X/Y/Z (B is symbolic link as precondition)
86+ * /A/B/C/D -> ../../D (non-recursive)
87+ * /A/B/C/E -> ../../Z (recursive)
88+ * }</pre>
89+ *
90+ * The starting path /A/B/C is already based on link. The real path of start is
91+ * /X/Y/Z/C so ../../Z points (recursive) to /X/Y/Z. /X/Y/D and /X/Y/Z is what
92+ * we expect to resolve but we fail in both cases with NoSuchFileException.
93+ */
94+ @ ParameterizedTest
95+ @ ValueSource (booleans = { false , true })
96+ public void testGithubBug2220 (boolean useAdvancedLinkCheck ) throws Exception {
97+ assumeTrue (canCreateSymLinks (), "only relevant for platforms supporting symbolic links" );
98+ assumeTrue (!OS .isWindows (), "Windows file system handles recursive links differently" );
99+ final boolean originalValue = UnifiedTree .isAdvancedRecursiveLinkChecksEnabled ();
100+ try {
101+ UnifiedTree .enableAdvancedRecursiveLinkChecks (useAdvancedLinkCheck );
102+ IProject project = getWorkspace ().getRoot ().getProject ("testGithubBug2220" );
103+ createInWorkspace (project );
104+
105+ /* Re-use projects which are cleaned up automatically */
106+ getWorkspace ().run ((IWorkspaceRunnable ) monitor -> {
107+ /* delete open project because we must re-open with BACKGROUND_REFRESH */
108+ project .delete (IResource .NEVER_DELETE_PROJECT_CONTENT , createTestMonitor ());
109+ project .create (null );
110+ try {
111+ createGithubBug2220Structure (EFS .getStore (project .getLocationURI ()));
112+ } catch (IOException e ) {
113+ throw new IllegalStateException ("unexpected IOException occurred" , e );
114+ }
115+ // Bug only happens with BACKGROUND_REFRESH.
116+ project .open (IResource .BACKGROUND_REFRESH , createTestMonitor ());
117+ }, null );
118+
119+ // wait for BACKGROUND_REFRESH to complete.
120+ waitForRefresh ();
121+ project .accept (new IResourceVisitor () {
122+ int resourceCount = 0 ;
123+
124+ @ Override
125+ public boolean visit (IResource resource ) {
126+ resourceCount ++;
127+ // We have 1 root + .settings + prefs + + .project + 10 elements --> 14 elements
128+ // to visit at most
129+ System .out .println (resourceCount + " visited: " + resource .getFullPath ());
130+ assertTrue (resourceCount <= 15 , "Expected max 15 elements to visit, got: " + resourceCount );
131+ return true ;
132+ }
133+ });
134+ } finally {
135+ UnifiedTree .enableAdvancedRecursiveLinkChecks (originalValue );
136+ }
137+ }
138+
139+ /**
140+ * <pre>{@code
141+ * /A/B -> /X/Y/Z (B is symbolic link as precondition)
142+ * /A/B/C/D -> ../../D (non-recursive)
143+ * /A/B/C/E -> ../../Z (recursive)
144+ * }</pre>
145+ *
146+ * The starting path /A/B/C is already based on link. The real path of start is
147+ * /X/Y/Z/C so ../../Z points (recursive) to /X/Y/Z.
148+ */
149+ protected void createGithubBug2220Structure (IFileStore rootDir ) throws CoreException , IOException {
150+ Path root = rootDir .toLocalFile (EFS .NONE , createTestMonitor ()).toPath ();
151+ Files .createDirectories (root .resolve ("A" ));
152+ Files .createDirectories (root .resolve ("X/Y/Z" ));
153+ Files .createDirectories (root .resolve ("X/Y/D" ));
154+ Files .createSymbolicLink (root .resolve ("A/B" ), root .resolve ("X/Y/Z" ));
155+ Files .createDirectories (root .resolve ("A/B/C" ));
156+ Files .createSymbolicLink (root .resolve ("A/B/C/D" ), Paths .get ("../../D" ));
157+ Files .createSymbolicLink (root .resolve ("A/B/C/E" ), Paths .get ("../../Z" ));
158+ }
159+
73160 /**
74161 * Test a very specific case of mutually recursive symbolic links:
75162 * <pre> {@code
@@ -85,7 +172,7 @@ protected void createBug358830Structure(IFileStore rootDir) throws CoreException
85172 */
86173 @ Test
87174 public void testBug232426 () throws Exception {
88- assumeTrue ("only relevant for platforms supporting symbolic links" , canCreateSymLinks () );
175+ assumeTrue (canCreateSymLinks (), "only relevant for platforms supporting symbolic links" );
89176
90177 IProject project = getWorkspace ().getRoot ().getProject ("Project" );
91178 createInWorkspace (project );
@@ -118,36 +205,42 @@ public boolean visit(IResource resource) {
118205 });
119206 }
120207
121- @ Test
122- public void testBug358830 () throws Exception {
123- assumeTrue ("only relevant for platforms supporting symbolic links" , canCreateSymLinks ());
124-
125- IProject project = getWorkspace ().getRoot ().getProject ("Project" );
126- createInWorkspace (project );
127- /* Re-use projects which are cleaned up automatically */
128- getWorkspace ().run ((IWorkspaceRunnable ) monitor -> {
129- /* delete open project because we must re-open with BACKGROUND_REFRESH */
130- project .delete (IResource .NEVER_DELETE_PROJECT_CONTENT , createTestMonitor ());
131- project .create (null );
132- try {
133- createBug358830Structure (EFS .getStore (project .getLocationURI ()));
134- } catch (IOException e ) {
135- throw new IllegalStateException ("unexpected IOException occurred" , e );
136- }
137- project .open (IResource .BACKGROUND_REFRESH , createTestMonitor ());
138- }, null );
139-
140- //wait for BACKGROUND_REFRESH to complete.
141- waitForRefresh ();
142- final int resourceCount [] = new int [] {0 };
143- project .accept (resource -> {
144- resourceCount [0 ]++;
145- return true ;
146- });
147- // We have 1 root + 1 folder + 1 file (.project)
148- // + .settings / resources prefs
149- // --> 5 elements to visit
150- assertEquals (5 , resourceCount [0 ]);
208+ @ ParameterizedTest
209+ @ ValueSource (booleans = { false , true })
210+ public void testBug358830 (boolean useAdvancedLinkCheck ) throws Exception {
211+ assumeTrue (canCreateSymLinks (), "only relevant for platforms supporting symbolic links" );
212+ final boolean originalValue = UnifiedTree .isAdvancedRecursiveLinkChecksEnabled ();
213+ try {
214+ UnifiedTree .enableAdvancedRecursiveLinkChecks (useAdvancedLinkCheck );
215+ IProject project = getWorkspace ().getRoot ().getProject ("Project" );
216+ createInWorkspace (project );
217+ /* Re-use projects which are cleaned up automatically */
218+ getWorkspace ().run ((IWorkspaceRunnable ) monitor -> {
219+ /* delete open project because we must re-open with BACKGROUND_REFRESH */
220+ project .delete (IResource .NEVER_DELETE_PROJECT_CONTENT , createTestMonitor ());
221+ project .create (null );
222+ try {
223+ createBug358830Structure (EFS .getStore (project .getLocationURI ()));
224+ } catch (IOException e ) {
225+ throw new IllegalStateException ("unexpected IOException occurred" , e );
226+ }
227+ project .open (IResource .BACKGROUND_REFRESH , createTestMonitor ());
228+ }, null );
229+
230+ // wait for BACKGROUND_REFRESH to complete.
231+ waitForRefresh ();
232+ final int resourceCount [] = new int [] { 0 };
233+ project .accept (resource -> {
234+ resourceCount [0 ]++;
235+ return true ;
236+ });
237+ // We have 1 root + 1 folder + 1 file (.project)
238+ // + .settings / resources prefs
239+ // --> 5 elements to visit
240+ assertEquals (5 , resourceCount [0 ]);
241+ } finally {
242+ UnifiedTree .enableAdvancedRecursiveLinkChecks (originalValue );
243+ }
151244 }
152245
153246}
0 commit comments