64675c24b3136d06f2123a1039a034114bd597b6
lrnassar
  Thu Feb 12 13:35:41 2026 -0800
Improving error messages mostly on hubCheck, with a few trackHub.c cases. Overall making the messages more informative to tell users what went wrong and what they can do to fix it, refs #35718

diff --git src/hg/utils/hubCheck/hubCheck.c src/hg/utils/hubCheck/hubCheck.c
index 9ac7eebd5a6..62a38e38f57 100644
--- src/hg/utils/hubCheck/hubCheck.c
+++ src/hg/utils/hubCheck/hubCheck.c
@@ -435,31 +435,31 @@
 if (options->extra && hashLookup(options->extra, setting))
         return 0;
 
 /* check setting is supported in this version */
 struct trackHubSettingSpec *hubSetting = hashFindVal(options->settings, setting);
 
 boolean hasComplexSetting = isComplexSetting(setting);
 if (hasComplexSetting)
     {
     verbose(5, "skipping validation for complex setting=%s.", setting);
     return 1;
     }
 
 if (hubSetting == NULL)
     {
-    dyStringPrintf(errors, "Setting '%s' is unknown/unsupported", setting);
+    dyStringPrintf(errors, "Setting '%s' is not recognized. Check for typos, or see https://genome.ucsc.edu/goldenPath/help/trackDb/trackDbHub.html for supported settings", setting);
     char *suggest = suggestSetting(setting, options);
     if (suggest != NULL)
         dyStringPrintf(errors, " (did you mean '%s' ?)", suggest);
     dyStringPrintf(errors, "\n");
     retVal = 1;
     }
 else if (sameString(hubSetting->level, "deprecated"))
     {
     dyStringPrintf(errors, "Setting '%s' is deprecated\n", setting);
     retVal = 1;
     }
 else
     {
     /*  check level */
     struct trackHubSettingSpec *checkLevel = NULL;
@@ -637,50 +637,50 @@
 
 int i;
 char *subtrackName = trackHubSkipHubName(tdb->track);
 sortOrder_t *sortOrder = NULL;
 membership_t *membership = NULL;
 membersForAll_t *membersForAll = NULL;
 struct errCatch *errCatch = errCatchNew();
 
 if (errCatchStart(errCatch))
     {
     // check that subtrack group is the same as the parent group:
     char *subTrackGroup = tdb->grp;
     char *parentGroup = tdb->parent->grp;
     if (!sameString(subTrackGroup, parentGroup))
         {
-        errAbort("assembly %s: track %s has a different group (%s) than parent %s (group %s). Please specify the group setting in both the parent and the subtrack stanzas", trackHubSkipHubName(genome->name), subtrackName, subTrackGroup, trackHubSkipHubName(tdb->parent->track), parentGroup);
+        errAbort("assembly %s: subtrack \"%s\" has 'group %s' but its parent \"%s\" has 'group %s'. Subtracks inherit their parent's group. Either remove the 'group' line from the subtrack, or set both parent and subtrack to the same group.", trackHubSkipHubName(genome->name), subtrackName, subTrackGroup, trackHubSkipHubName(tdb->parent->track), parentGroup);
         }
 
     // check subgroups settings
     membersForAll = membersForAllSubGroupsGet(tdb->parent, NULL);
 
     // membersForAllSubGroupsGet() warns about the parent stanza, turn it into an errAbort
     if (errCatch->gotWarning)
         {
         errAbort("%s", errCatch->message->string);
         }
 
     if (membersForAll && checkEmptyMembersForAll(membersForAll, tdb->parent))
         {
         membership = subgroupMembershipGet(tdb);
         sortOrder = sortOrderGet(NULL, tdb->parent);
 
         if (membership == NULL)
             {
-            errAbort("missing 'subgroups' setting for subtrack %s", subtrackName);
+            errAbort("missing 'subgroups' setting for subtrack %s. Add a 'subGroups' line declaring this subtrack's group membership, e.g. 'subGroups view=signal'", subtrackName);
             }
 
         // if a sortOrder is defined, make sure every subtrack has that membership
         if (sortOrder)
             {
             for (i = 0; i < sortOrder->count; i++)
                 {
                 char *col = sortOrder->column[i];
                 if ( (!sameString(col, SUBTRACK_COLOR_SUBGROUP)) && (membership == NULL || stringArrayIx(col, membership->subgroups, membership->count) == -1))
                     warn("%s not a member of sortOrder subgroup %s", subtrackName, col);
                 }
             }
 
         // now check that this subtrack is a member of every defined subgroup
         for (i = 0; i < ArraySize(membersForAll->members); i++)
@@ -689,31 +689,31 @@
                 {
                 char *subgroupName = membersForAll->members[i]->groupTag;
                 if (stringArrayIx(subgroupName, membership->subgroups, membership->count) == -1)
                     {
                     warn("subtrack %s not a member of subgroup %s", subtrackName, subgroupName);
                     }
                 }
             }
 
         // check that the subtrack does not have any bogus subgroups that don't exist in the parent
         for (i = 0; i < membership->count; i++)
             {
             char *subgroupName = membership->subgroups[i];
             if (!subgroupingExists(tdb->parent, subgroupName))
                 {
-                warn("subtrack \"%s\" has a subgroup \"%s\" not defined at parent level", subtrackName, subgroupName);
+                warn("subtrack \"%s\" declares subgroup \"%s\", but the parent composite does not define this subgroup. Check the 'subGroup' lines in the parent stanza for a matching group name.", subtrackName, subgroupName);
                 }
             }
         }
     }
 errCatchEnd(errCatch);
 if (errCatch->gotError || errCatch->gotWarning)
     {
     retVal = 1;
     trackDbErr(errors, errCatch->message->string, genome, tdb, options->htmlOut);
     }
 errCatchFree(&errCatch);
 
 return retVal;
 }
 
@@ -757,32 +757,34 @@
 
 void hubCheckParentsAndChildren(struct trackDb *tdb)
 /* Check that a single trackDb stanza has the correct parent and subtrack pointers */
 {
 if (tdbIsSuper(tdb) || tdbIsComposite(tdb) || tdbIsCompositeView(tdb) || tdbIsContainer(tdb))
     {
     if (tdb->subtracks == NULL)
         {
         errAbort("Track \"%s\" is declared superTrack, compositeTrack, view or "
             "container, but has no subtracks", tdb->track);
         }
 
     // Containers should not have a bigDataUrl setting
     if (trackDbLocalSetting(tdb, "bigDataUrl"))
         {
-        errAbort("Track \"%s\" is declared superTrack, compositeTrack, view or "
-            "container, and also has a bigDataUrl", tdb->track);
+        errAbort("Track \"%s\" is a container (compositeTrack/superTrack/view) but also has "
+            "a 'bigDataUrl'. Container tracks organize subtracks and should not have data files. "
+            "Remove 'bigDataUrl' from this stanza, or remove the container declaration if this is "
+            "a data track.", tdb->track);
         }
 
     // multiWigs cannot be the child of a composite
     if (tdbIsContainer(tdb) &&
             (tdb->parent != NULL &&
             (tdbIsComposite(tdb->parent) || tdbIsCompositeView(tdb->parent))))
         {
         errAbort("Track \"%s\" is declared container multiWig and has parent \"%s\"."
             " Container multiWig tracks cannot be children of composites or views",
             tdb->track, tdb->parent->track);
         }
     }
 else if (tdb->subtracks != NULL)
     {
     errAbort("Track \"%s\" has children tracks (e.g: \"%s\"), but is not a "
@@ -804,31 +806,31 @@
     if (!isParentTrack &&
             // these are the valid trackDb types for hub data tracks:
             !(sameString("bigNarrowPeak", trackType) || sameString("bigBed", trackType) ||
                     sameString("bigGenePred", trackType)  || sameString("bigPsl", trackType)||
                     sameString("bigChain", trackType)|| sameString("bigMaf", trackType) ||
                     sameString("bigBarChart", trackType) || sameString("bigInteract", trackType) ||
                     sameString("bigLolly", trackType) || sameString("bigRmsk", trackType) ||
                     sameString("bigWig", trackType) || sameString("longTabix", trackType) ||
                     sameString("vcfTabix", trackType) || sameString("vcfPhasedTrio", trackType) ||
                     sameString("bam", trackType) || sameString("hic", trackType)
             #ifdef USE_HAL
                     || sameString("halSnake", trackType)
             #endif
             ))
         {
-        errAbort("error in type line \"%s\" for track \"%s\". The only valid types for tracks that are not composites, views or supertracks are: bigWig, bigBed and bigBed variants like bigGenePred/bigChain/bigBarChart/etc, longTabix, vcfTabix, vcfPhasedTrio, bam, hic and halSnake.", trackType, tdb->track);
+        errAbort("error: unrecognized type \"%s\" for track \"%s\". Valid types include: bigWig, bigBed, bigGenePred, bigPsl, bigChain, bigMaf, bigBarChart, bigInteract, vcfTabix, bam, hic, and longTabix. See https://genome.ucsc.edu/goldenPath/help/trackDb/trackDbHub.html#type for the full list.", trackType, tdb->track);
         }
 
     if (sameString(splitType[0], "bigBed"))
         {
         if (numWords > 1 && (strchr(splitType[1], '+') || strchr(splitType[1], '.')))
             {
             errAbort("error in type line \"%s\" for track \"%s\". "
                 "A space is needed after the \"+\" or \".\" character.", type, tdb->track);
             }
         if (numWords > 2 && (!sameString(splitType[2], "+") && !sameString(splitType[2], ".")))
             {
             errAbort("error in type line \"%s\" for track \"%s\". "
                 "Only \"+\" or \".\" is allowed after bigBed numFields setting.", type, tdb->track);
             }
         }
@@ -875,31 +877,31 @@
     char *lower = words[0];
     double lowerVal = 0;
     boolean validLower = isValidDouble(lower, &lowerVal);
     if (!validLower)
 	errAbort("Invalid double range lower value '%s' in range '%s' in setting %s track %s", lower, setting, settingName, tdb->track);
 
     char *upper = words[1];
     double upperVal = 0;
     boolean validUpper = isValidDouble(upper, &upperVal);
     if (!validUpper)
 	errAbort("Invalid double range upper value '%s' in range '%s' in setting %s track %s", upper, setting, settingName, tdb->track);
 
     if (validUpper && validLower)
 	{
 	if (upperVal < lowerVal)
-	    warn("upper < lower. Should swap lower and upper range values in '%s' in setting %s track %s", setting, settingName, tdb->track);
+	    warn("in track \"%s\", setting '%s' has the range '%s' where the upper bound is less than the lower bound. Swap the values so the format is lower:upper (e.g. 'viewLimits 0:100').", tdb->track, settingName, setting);
 	}
     }
 else
     errAbort("Missing a colon in range value '%s' in setting %s track %s", setting, settingName, tdb->track);
 }
 
 void checkViewLimitsSettings(struct trackDb *tdb)
 /* check viewLimits and viewLimitsMax and defaultViewLimits setting values */
 {
 int i;
 for(i = 0; i < 3; i++)
     {
     char *settingName;
     if (i == 0)
 	settingName = VIEWLIMITS;
@@ -1000,36 +1002,34 @@
     checkViewLimitsSettings(tdb);
 
     if (!sameString(tdb->track, "cytoBandIdeo"))
         {
         trackHubAddDescription(genome->trackDbFile, tdb);
         if (!tdb->html)
             warn("warning: missing description page for track. Add 'html %s.html' line to the '%s' track stanza. ",
                  tdb->track, tdb->track);
         }
 
     if (!trackIsContainer && sameString(trackDbRequiredSetting(tdb, "type"), "bigWig"))
         {
         char *autoScaleSetting = trackDbLocalSetting(tdb, "autoScale");
         if (autoScaleSetting && !sameString(autoScaleSetting, "off") && !sameString(autoScaleSetting, "on"))
             {
-            errAbort("track \"%s\" has value \"%s\" for autoScale setting, "
-                    "valid autoScale values for individual bigWig tracks are \"off\" or \"on\" only. "
-                    "If \"%s\" is part of a bigWig composite track and you want to use the "
-                    "\"%s\" setting, only declare \"autoScale group\" in the parent stanza",
-                    trackHubSkipHubName(tdb->track), autoScaleSetting, trackHubSkipHubName(tdb->track), 
-                    autoScaleSetting);
+            errAbort("track \"%s\" uses 'autoScale %s', but individual bigWig tracks only accept "
+                    "'autoScale on' or 'autoScale off'. To use 'autoScale %s', move that setting to the "
+                    "parent composite stanza instead.",
+                    trackHubSkipHubName(tdb->track), autoScaleSetting, autoScaleSetting);
             }
         }
     }
 errCatchEnd(errCatch);
 if (errCatch->gotError || errCatch->gotWarning)
     {
     retVal = 1;
     trackDbErr(errors, errCatch->message->string, genome, tdb, options->htmlOut);
     if (errCatch->gotError)
         trackDbErrorCount += 1;
     }
 errCatchFree(&errCatch);
 
 if (options->htmlOut)
     {
@@ -1171,33 +1171,33 @@
 /* Check a track data hub for integrity. Put errors in dyString.
  *      return 0 if hub has no errors, 1 otherwise
  *      if options->checkTracks is TRUE, check remote files of individual tracks
  */
 {
 struct errCatch *errCatch = errCatchNew();
 struct trackHub *hub = NULL;
 struct dyString *hubErrors = dyStringNew(0);
 int retVal = 0;
 
 if (errCatchStart(errCatch))
     {
     hub = trackHubOpen(hubUrl, "");
     char *descUrl = hub->descriptionUrl;
     if (descUrl == NULL)
-        warn("warning: missing hub overview description page (descriptionUrl setting)");
+        warn("warning: missing hub overview description page. Add a 'descriptionUrl hubDescription.html' line to hub.txt.");
     else if (!extFileExists(descUrl))
-        warn("warning: %s descriptionUrl setting does not exist", hub->descriptionUrl);
+        warn("warning: descriptionUrl '%s' could not be accessed. Verify the file exists at the specified path and is publicly readable.", hub->descriptionUrl);
     }
 errCatchEnd(errCatch);
 if (errCatch->gotError || errCatch->gotWarning)
     {
     retVal = 1;
     hubErr(hubErrors, errCatch->message->string, hub, options->htmlOut);
 
     if (options->htmlOut)
         {
         if (hub && hub->shortLabel)
             {
             dyStringPrintf(errors, "trackData['#'] = [%s,",
                 makeFolderObjectString(hub->shortLabel, "Hub Errors", "#",
                     "Click to open node", TRUE, TRUE));
             }