| /******************************************************************************* |
| * Copyright (c) 2008, 2015 Broadcom Corporation and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * James Blackburn (Broadcom Corp.) - Initial API and implementation |
| *******************************************************************************/ |
| |
| package org.eclipse.cdt.core.settings.model; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.FileOutputStream; |
| import java.util.HashSet; |
| import java.util.Set; |
| |
| import org.eclipse.cdt.core.CCorePlugin; |
| import org.eclipse.cdt.core.dom.IPDOMManager; |
| import org.eclipse.cdt.core.model.CoreModel; |
| import org.eclipse.cdt.core.model.ICProject; |
| import org.eclipse.cdt.core.testplugin.CProjectHelper; |
| import org.eclipse.cdt.core.testplugin.util.BaseTestCase; |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.core.resources.IFolder; |
| import org.eclipse.core.resources.IProject; |
| import org.eclipse.core.resources.IResource; |
| import org.eclipse.core.resources.IResourceChangeEvent; |
| import org.eclipse.core.resources.IResourceChangeListener; |
| import org.eclipse.core.resources.IResourceDelta; |
| import org.eclipse.core.resources.IResourceVisitor; |
| import org.eclipse.core.resources.ResourcesPlugin; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IPath; |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.core.runtime.jobs.Job; |
| import org.eclipse.core.runtime.preferences.InstanceScope; |
| |
| import junit.framework.TestSuite; |
| |
| /** |
| * Testsuite for the project description storage. This |
| * currently tests some of the features of the built-in |
| * XmlProjectDescriptionStorage(2) |
| */ |
| public class CProjectDescriptionStorageTests extends BaseTestCase { |
| |
| /** CProject on which these tests are based */ |
| ICProject cProj; |
| |
| public static TestSuite suite() { |
| return suite(CProjectDescriptionStorageTests.class, "_"); |
| } |
| |
| // resource change listener that will listen for file changes interesting to the tests |
| OurResourceChangeListener resListener; |
| |
| @Override |
| protected void setUp() throws Exception { |
| cProj = CProjectHelper.createNewStyleCProject("CProjDescStorage", IPDOMManager.ID_FAST_INDEXER); |
| resListener = new OurResourceChangeListener(); |
| ResourcesPlugin.getWorkspace().addResourceChangeListener(resListener); |
| } |
| |
| @Override |
| protected void tearDown() throws Exception { |
| // Remover our resource change listener |
| ResourcesPlugin.getWorkspace().removeResourceChangeListener(resListener); |
| // Make the project files writable so they can be deleted... |
| cProj.getProject().refreshLocal(IResource.DEPTH_INFINITE, null); |
| cProj.getProject().getFile(".cproject").setReadOnly(false); |
| if (cProj.getProject().getFolder(".csettings").exists()) { |
| cProj.getProject().getFolder(".csettings").setReadOnly(false); |
| for (IResource child : cProj.getProject().getFolder(".csettings").members()) |
| child.setReadOnly(false); |
| } |
| // Wait for a few seconds for the indexer to get going so we avoid deadlock |
| Thread.sleep(2000); |
| // Delete the project |
| CProjectHelper.delete(cProj); |
| } |
| |
| /** |
| * Tests that external modifications to the CProjectDescription file are picked up |
| * @throws Exception |
| */ |
| public void testExternalCProjDescModification() throws Exception { |
| // Create auto-refresh Thread |
| Job refreshJob = new Job("Auto-Refresh") { |
| @Override |
| protected IStatus run(IProgressMonitor monitor) { |
| if (monitor.isCanceled()) |
| return Status.CANCEL_STATUS; |
| try { |
| ResourcesPlugin.getWorkspace().getRoot().refreshLocal(IResource.DEPTH_INFINITE, null); |
| } catch (CoreException e) { |
| fail("Error during refresh: " + e.getMessage()); |
| } |
| schedule(500); |
| return Status.OK_STATUS; |
| } |
| }; |
| refreshJob.schedule(); |
| |
| // Backup the CProjectFile |
| final String initial = "initial"; |
| final String testingStorage = "testingStorage"; |
| final String testChildInStorage = "testChildInStorage"; |
| |
| // Backup the original storage file |
| backUpCProjectFile(initial); |
| |
| IProject project = cProj.getProject(); |
| ICProjectDescription projDesc = CoreModel.getDefault().getProjectDescription(project, true); |
| projDesc.getDefaultSettingConfiguration().getStorage(testingStorage, true).createChild(testChildInStorage); |
| CoreModel.getDefault().setProjectDescription(project, projDesc); |
| // backup this project_desc |
| backUpCProjectFile(testingStorage); |
| |
| // Close and open project |
| project.close(null); |
| project.open(null); |
| |
| // verify changes are in read-only description |
| projDesc = CoreModel.getDefault().getProjectDescription(project, false); |
| assertNotNull(projDesc.getDefaultSettingConfiguration().getStorage(testingStorage, false)); |
| project.refreshLocal(IResource.DEPTH_INFINITE, null); |
| |
| // Restore from backup |
| resListener.reset(); |
| resListener.addFileToWatch(cProj.getProject().getFile(".cproject").getFullPath()); |
| restoreCProjectFile(initial); |
| resListener.waitForChange(); |
| |
| // Fetch what should be the initial project description |
| projDesc = CoreModel.getDefault().getProjectDescription(project, false); |
| assertNull(projDesc.getDefaultSettingConfiguration().getStorage(testingStorage, false)); |
| |
| // Test XmlProjectDescriptionStorage2: |
| |
| // Test that updating the contents of a storage module leads to a reload of the project description |
| // (In XmlProjectDescriptionStorage2 configuration storage modules are stored in different files...) |
| restoreCProjectFile(testingStorage); |
| project.close(null); |
| project.open(null); |
| |
| // create testChildInStorage |
| projDesc = CoreModel.getDefault().getProjectDescription(project, true); |
| ICStorageElement[] children = projDesc.getDefaultSettingConfiguration().getStorage(testingStorage, false) |
| .getChildrenByName(testChildInStorage); |
| assertTrue(children.length == 1); |
| projDesc.getDefaultSettingConfiguration().getStorage(testingStorage, false).removeChild(children[0]); |
| CoreModel.getDefault().setProjectDescription(project, projDesc); |
| projDesc = CoreModel.getDefault().getProjectDescription(project, false); |
| assertTrue(projDesc.getDefaultSettingConfiguration().getStorage(testingStorage, false) |
| .getChildrenByName(testChildInStorage).length == 0); |
| |
| project.refreshLocal(IResource.DEPTH_INFINITE, null); |
| // Restore from backup |
| resListener.reset(); |
| resListener.addFileToWatch(cProj.getProject().getFolder(".csettings").getFullPath()); |
| restoreCProjectFile(testingStorage); |
| resListener.waitForChange(); |
| |
| // Check that the project description no longer contains the testChildInStorage |
| projDesc = CoreModel.getDefault().getProjectDescription(project, false); |
| assertTrue(projDesc.getDefaultSettingConfiguration().getStorage(testingStorage, false) |
| .getChildrenByName(testChildInStorage).length == 1); |
| |
| refreshJob.cancel(); |
| } |
| |
| /** |
| * Tests that external create and replace of CProjectDescription is picked up |
| * (Bug 311189) |
| * @throws Exception |
| */ |
| public void testExternalCProjDescRemoveAndReplace() throws Exception { |
| // Create auto-refresh Thread |
| Job refreshJob = new Job("Auto-Refresh") { |
| @Override |
| protected IStatus run(IProgressMonitor monitor) { |
| if (monitor.isCanceled()) |
| return Status.CANCEL_STATUS; |
| try { |
| ResourcesPlugin.getWorkspace().getRoot().refreshLocal(IResource.DEPTH_INFINITE, null); |
| } catch (CoreException e) { |
| fail("Error during refresh: " + e.getMessage()); |
| } |
| schedule(500); |
| return Status.OK_STATUS; |
| } |
| }; |
| refreshJob.schedule(); |
| |
| // Backup the CProjectFile |
| final String initial = "initial"; |
| final String testingStorage = "testingStorage"; |
| final String testChildInStorage = "testChildInStorage"; |
| |
| // Backup the original storage file |
| backUpCProjectFile(initial); |
| |
| IProject project = cProj.getProject(); |
| ICProjectDescription projDesc = CoreModel.getDefault().getProjectDescription(project, true); |
| projDesc.getDefaultSettingConfiguration().getStorage(testingStorage, true).createChild(testChildInStorage); |
| CoreModel.getDefault().setProjectDescription(project, projDesc); |
| // backup this project_desc |
| backUpCProjectFile(testingStorage); |
| |
| // Close and open project |
| project.close(null); |
| project.open(null); |
| |
| // verify changes are in read-only description |
| projDesc = CoreModel.getDefault().getProjectDescription(project, false); |
| assertNotNull(projDesc.getDefaultSettingConfiguration().getStorage(testingStorage, false)); |
| project.refreshLocal(IResource.DEPTH_INFINITE, null); |
| |
| try { |
| // Lock the workspace |
| Job.getJobManager().beginRule(ResourcesPlugin.getWorkspace().getRoot(), null); |
| |
| // Restore from backup |
| resListener.reset(); |
| resListener.addFileToWatch(cProj.getProject().getFile(".cproject").getFullPath()); |
| restoreCProjectUsingIResource(initial); |
| } finally { |
| Job.getJobManager().endRule(ResourcesPlugin.getWorkspace().getRoot()); |
| } |
| |
| resListener.waitForChange(); |
| // Fetch what should be the initial project description |
| projDesc = CoreModel.getDefault().getProjectDescription(project, false); |
| assertNull(projDesc.getDefaultSettingConfiguration().getStorage(testingStorage, false)); |
| |
| refreshJob.cancel(); |
| } |
| |
| /** |
| * Tests that a read-only project description file is picked up |
| * @throws Exception |
| */ |
| public void testReadOnlyProjectDescription() throws Exception { |
| enableSetWritableWhenHeadless(true); |
| try { |
| makeDescriptionReadOnly(); |
| IProject project = cProj.getProject(); |
| ICProjectDescription projDesc = CoreModel.getDefault().getProjectDescription(project, true); |
| projDesc.getDefaultSettingConfiguration().getStorage("Temp_testing_storage", true); |
| CoreModel.getDefault().setProjectDescription(project, projDesc); |
| |
| project.close(null); |
| project.open(null); |
| |
| projDesc = CoreModel.getDefault().getProjectDescription(project, false); |
| assertNotNull(projDesc.getDefaultSettingConfiguration().getStorage("Temp_testing_storage", false)); |
| projDesc = CoreModel.getDefault().getProjectDescription(project, true); |
| makeDescriptionReadOnly(); |
| projDesc.getDefaultSettingConfiguration().removeStorage("Temp_testing_storage"); |
| CoreModel.getDefault().setProjectDescription(project, projDesc); |
| |
| project.close(null); |
| project.open(null); |
| |
| projDesc = CoreModel.getDefault().getProjectDescription(project, false); |
| assertNull(projDesc.getDefaultSettingConfiguration().getStorage("Temp_testing_storage", false)); |
| } finally { |
| enableSetWritableWhenHeadless(false); |
| } |
| } |
| |
| /* |
| * |
| * Helper methods for external modifications |
| * |
| */ |
| |
| /** |
| * Enables/disables team UI preference whether validateEdit should |
| * set files writable if no UI context has been provided. |
| */ |
| private void enableSetWritableWhenHeadless(boolean enable) { |
| InstanceScope.INSTANCE.getNode("org.eclipse.team.ui") |
| .putBoolean("org.eclipse.team.ui.validate_edit_with_no_context", enable); |
| } |
| |
| /** |
| * makes the project description (as stored by the XmlProjectDescriptionStorage & |
| * XmlProjectDescriptionStorage2) read-only. Does this using java.io.File deliberately. |
| */ |
| private void makeDescriptionReadOnly() throws Exception { |
| File cproj = cProj.getProject().getFile(".cproject").getLocation().toFile(); |
| if (!cproj.exists()) |
| throw new FileNotFoundException(); |
| cproj.setReadOnly(); |
| // XmlProjectDescription2 stores settings in a .csettings directory, look for it |
| File csettDir = cProj.getProject().getFile(".csettings").getLocation().toFile(); |
| if (csettDir.exists()) { |
| for (File child : csettDir.listFiles()) |
| child.setReadOnly(); |
| csettDir.setReadOnly(); |
| } |
| } |
| |
| /** |
| * Restore the file from the backup. Only does so if the contents of the file |
| * have changed (to prevent updating the modification stamp on the file...) |
| * @param uniqueKey |
| */ |
| private void restoreCProjectFile(String uniqueKey) { |
| File cproj = cProj.getProject().getFile(".cproject").getLocation().toFile(); |
| File cprojback = cProj.getProject().getFile(".cproject_" + uniqueKey).getLocation().toFile(); |
| if (diff(cprojback, cproj)) |
| copyFile(cprojback, cproj); |
| File csettings = cProj.getProject().getFile(".csettings").getLocation().toFile(); |
| File csettingsback = cProj.getProject().getFile(".csettings_" + uniqueKey).getLocation().toFile(); |
| if (csettingsback.exists()) { |
| for (File f : csettingsback.listFiles()) { |
| File orig = new File(csettings, f.getName()); |
| if (diff(f, orig)) |
| copyFile(f, orig); |
| } |
| } |
| } |
| |
| /** |
| * Use IResource API to remove and replace the .cproject file (rather than just modifying |
| * it atomically exteranlly) |
| * This tests the team provider remove and replace behaviour when holding a higher-leve resource lock |
| */ |
| private void restoreCProjectUsingIResource(String uniqueKey) throws CoreException { |
| // delete the .cproject |
| IFile cproject = cProj.getProject().getFile(".cproject"); |
| IFile cproject_back = cProj.getProject().getFile(".cproject_" + uniqueKey); |
| cproject.delete(true, null); |
| cproject.create(cproject_back.getContents(true), true, null); |
| |
| final IFolder csettings = cProj.getProject().getFolder(".csettings"); |
| IFolder csettings_back = cProj.getProject().getFolder(".csettings_" + uniqueKey); |
| csettings.refreshLocal(IResource.DEPTH_INFINITE, null); |
| csettings_back.refreshLocal(IResource.DEPTH_INFINITE, null); |
| |
| // Nothing to do if these directories don't exist |
| if (!csettings.exists() && !csettings_back.exists()) |
| return; |
| |
| csettings.delete(false, null); |
| csettings.create(true, false, null); |
| |
| csettings_back.accept(new IResourceVisitor() { |
| @Override |
| public boolean visit(IResource resource) throws CoreException { |
| assertTrue(resource instanceof IFile); |
| csettings.getFile(resource.getName()).create(((IFile) resource).getContents(), false, null); |
| return false; |
| } |
| }); |
| } |
| |
| private void backUpCProjectFile(String uniqueKey) { |
| File cproj = cProj.getProject().getFile(".cproject").getLocation().toFile(); |
| File cprojback = cProj.getProject().getFile(".cproject_" + uniqueKey).getLocation().toFile(); |
| copyFile(cproj, cprojback); |
| // backup .csettings as well |
| File csettings = cProj.getProject().getFile(".csettings").getLocation().toFile(); |
| if (csettings.exists() && csettings.isDirectory()) { |
| File csettingsback = cProj.getProject().getFile(".csettings_" + uniqueKey).getLocation().toFile(); |
| if (!csettingsback.exists()) |
| csettingsback.mkdir(); |
| for (File f : csettings.listFiles()) |
| copyFile(f, new File(csettingsback, f.getName())); |
| } |
| } |
| |
| /** |
| * Return boolean indicating whether two files are different |
| * @param src1 |
| * @param src2 |
| * @return |
| */ |
| private boolean diff(File src1, File src2) { |
| if (!src1.exists() || !src2.exists()) |
| return true; |
| FileInputStream in1 = null; |
| FileInputStream in2 = null; |
| try { |
| in1 = new FileInputStream(src1); |
| in2 = new FileInputStream(src2); |
| while (true) { |
| int byteRead1 = in1.read(); |
| int byteRead2 = in2.read(); |
| if (byteRead1 == -1 && byteRead2 == -1) |
| return false; |
| if (byteRead1 != byteRead2) |
| return true; |
| } |
| } catch (Exception e) { |
| fail("Exception diffingFiles: " + src1.getAbsolutePath() + " ; " + src2.getAbsolutePath()); |
| return true; |
| } finally { |
| if (in1 != null) |
| try { |
| in1.close(); |
| } catch (Exception e) { |
| /*Don't care*/} |
| if (in2 != null) |
| try { |
| in2.close(); |
| } catch (Exception e) { |
| /*Don't care*/} |
| } |
| } |
| |
| private void copyFile(File src, File dst) { |
| long initModificationTime = dst.lastModified(); |
| FileInputStream in = null; |
| FileOutputStream out = null; |
| try { |
| in = new FileInputStream(src); |
| out = new FileOutputStream(dst); |
| byte[] buffer = new byte[8192]; |
| int bytesRead; |
| while ((bytesRead = in.read(buffer)) != -1) |
| out.write(buffer, 0, bytesRead); |
| } catch (Exception e) { |
| fail("Exception copyingFile: " + src.getAbsolutePath() + " -> " + dst.getAbsolutePath()); |
| } finally { |
| if (in != null) |
| try { |
| in.close(); |
| } catch (Exception e) { |
| /*Don't care*/} |
| if (out != null) |
| try { |
| out.close(); |
| } catch (Exception e) { |
| /*Don't care*/} |
| } |
| |
| while (dst.lastModified() - initModificationTime == 0) { |
| // Unix stat doesn't return granularity < 1000ms :( |
| // If we don't sleep here, and the unit test goes too quickly, we're scuppered. |
| try { |
| Thread.sleep(1000); |
| } catch (InterruptedException e) { |
| // Don't care |
| } |
| dst.setLastModified(System.currentTimeMillis()); |
| } |
| } |
| |
| /** |
| * Our resource change listener which notified us when a file has been detected as changed |
| * Users add files to the files to watch. Reset the listener and block waiting for the change to |
| * be noticed. |
| */ |
| private static class OurResourceChangeListener implements IResourceChangeListener { |
| boolean changeDetected; |
| private Set<IPath> filesToWatch = new HashSet<>(); |
| |
| @Override |
| public synchronized void resourceChanged(IResourceChangeEvent event) { |
| IResourceDelta delta = event.getDelta(); |
| if (delta == null) |
| return; |
| for (IPath f : filesToWatch) |
| if (delta.findMember(f) != null) { |
| changeDetected = true; |
| notifyAll(); |
| break; |
| } |
| } |
| |
| public synchronized void addFileToWatch(IPath file) { |
| filesToWatch.add(file); |
| } |
| |
| public synchronized void reset() { |
| changeDetected = false; |
| } |
| |
| public synchronized void waitForChange() { |
| try { |
| if (!changeDetected) |
| wait(20000); |
| if (!changeDetected) |
| CCorePlugin.log("No Change detected in 20s!"); |
| } catch (InterruptedException e) { |
| Thread.currentThread().interrupt(); |
| } |
| } |
| } |
| } |