Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -418,14 +418,61 @@ private void loadDelayedPlugins() {
delayedLoadPlugins.add(plugin);
} else {
failedPlugins.put(plugin.getName(), plugin);
LOG.error(
"ERROR: Plugin [{}] cannot be loaded because its dependencies [{}}] cannot be resolved",
plugin.getName(),
plugin.getDependsOnNames()
logUnresolvedDependencies(plugin);
}
}
}
}

private void logUnresolvedDependencies(PluginInfo plugin) {
var unresolvedDependencies = new ArrayList<String>();
for (var name : plugin.getDependsOnNames()) {
var requiredVersion = plugin.getMetadata().getDependentVersion(name);
var dependency = findPlugin(name);
if (dependency != null) {
if (!GrailsVersionUtils.isValidVersion(dependency.getPluginVersion(), requiredVersion)) {
unresolvedDependencies.add(
"dependency [" + name + "] has version [" + dependency.getPluginVersion() +
"] but requires [" + requiredVersion + "]"
);
}
} else if (getFailedPlugin(name) != null) {
unresolvedDependencies.add(
"dependency [" + name + "] with required version [" + requiredVersion + "] failed to load"
);
} else if (isDelayed(name)) {
unresolvedDependencies.add(
"dependency [" + name + "] with required version [" + requiredVersion +
"] is still pending load"
);
} else {
unresolvedDependencies.add(
"dependency [" + name + "] with required version [" + requiredVersion + "] is missing"
);
}
}
LOG.error(
"Grails plug-in [{}] with version [{}] cannot be loaded: {}",
plugin.getName(),
plugin.getPluginVersion(),
String.join("; ", unresolvedDependencies)
);
}

/**
* Checks whether a plugin of the given name is still waiting to be resolved.
*
* @param name the plugin name to look for
* @return {@code true} if a plugin with this name is currently in {@link #delayedLoadPlugins}
*/
private boolean isDelayed(String name) {
var normalizedName = PluginUtils.normalizePluginName(name);
for (var delayed : delayedLoadPlugins) {
if (PluginUtils.normalizePluginName(delayed.getName()).equals(normalizedName)) {
return true;
}
}
return false;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,98 @@ class RepeatedProbeGrailsPlugin {
cleanup:
System.setErr(originalErr)
}

def 'reports every missing or incompatible plugin dependency at ERROR'() {
given: 'a discovery bean with one available and one unresolved plugin'
def gcl = new GroovyClassLoader()
def availableDependencyClass = gcl.parseClass('''
class AvailableDependencyGrailsPlugin {
def version = "1.0.0"
}
''')
def unresolvedDependenciesClass = gcl.parseClass('''
class UnresolvedDependenciesGrailsPlugin {
def version = "4.0.0"
def dependsOn = [missingDependency: "1.0.0", availableDependency: "2.0.0"]
}
''')
def discovery = new DefaultPluginDiscovery(
[availableDependencyClass, unresolvedDependenciesClass] as Class<?>[]
)
discovery.loadPluginsFromClasspath = false

and: 'standard error is captured to observe slf4j-simple output'
def originalErr = System.err
def captured = new ByteArrayOutputStream()
System.setErr(new PrintStream(captured, true))

when: 'the public discovery API initializes the plugin set'
discovery.init(new StandardEnvironment())

then: 'the unresolved plugin remains failed and is not loaded'
discovery.hasFailedPlugin('unresolvedDependencies')
!discovery.hasPlugin('unresolvedDependencies')
discovery.getPluginsInLoadOrder()*.name == ['availableDependency']

and: 'one concise message at ERROR identifies the failed plugin and every unresolved dependency'
def errors = captured.toString().readLines().findAll { it.contains('unresolvedDependencies') }
errors.size() == 1
errors[0].contains('ERROR')
errors[0].contains('Grails plug-in [unresolvedDependencies] with version [4.0.0] cannot be loaded')
errors[0].contains('dependency [missingDependency] with required version [1.0.0] is missing')
errors[0].contains('dependency [availableDependency] has version [1.0.0] but requires [2.0.0]')

and: 'normal verbosity does not include a stack trace'
!captured.toString().contains('at org.apache.grails.core.plugins.DefaultPluginDiscovery')

cleanup:
System.setErr(originalErr)
}

def 'reports a dependency that failed to load as failed rather than missing'() {
given: 'a plugin whose only dependency itself fails to load due to a genuinely missing dependency'
def gcl = new GroovyClassLoader()
def failingDependencyClass = gcl.parseClass('''
class FailingDependencyGrailsPlugin {
def version = "1.0.0"
def dependsOn = [reallyMissing: "1.0.0"]
}
''')
def dependentClass = gcl.parseClass('''
class DependentOnFailedGrailsPlugin {
def version = "1.0.0"
def dependsOn = [failingDependency: "1.0.0"]
}
''')
def discovery = new DefaultPluginDiscovery(
[failingDependencyClass, dependentClass] as Class<?>[]
)
discovery.loadPluginsFromClasspath = false

and: 'standard error is captured to observe slf4j-simple output'
def originalErr = System.err
def captured = new ByteArrayOutputStream()
System.setErr(new PrintStream(captured, true))

when:
discovery.init(new StandardEnvironment())

then: 'both plugins end up failed'
discovery.hasFailedPlugin('failingDependency')
discovery.hasFailedPlugin('dependentOnFailed')

and: 'the missing root cause is reported as missing'
def rootCause = captured.toString().readLines().find { it.contains('[failingDependency]') }
rootCause.contains('dependency [reallyMissing] with required version [1.0.0] is missing')

and: 'the plugin depending on the failed plugin reports it as failed, not missing'
def dependent = captured.toString().readLines().find { it.contains('[dependentOnFailed]') }
dependent.contains('dependency [failingDependency] with required version [1.0.0] failed to load')
!dependent.contains('is missing')

cleanup:
System.setErr(originalErr)
}
}

// Test fixture plugin classes for GrailsPluginDiscoverySpec
Expand Down
Loading