Preliminary Setup

First we load the necessary libraries, along with a set of utility functions.

library(here)
library(stringr)
library(glue)
library(magrittr)
library(openxlsx)
library(SummarizedExperiment)
library(dplyr)
library(edgeR)
library(limma)
library(csaw)
library(sva)
library(ggplot2)
library(scales)
library(GGally)
library(ggalt)
library(ggthemes)
library(splines)
library(reshape2)
library(assertthat)
library(ggfortify)
library(broom)
library(ks)
library(RColorBrewer)

library(BSgenome.Hsapiens.UCSC.hg38)

library(doParallel)
ncores <- getOption("mc.cores", default=parallel::detectCores(logical = FALSE))
options(mc.cores=ncores)
registerDoParallel(cores=ncores)
library(BiocParallel)
register(DoparParam())

options(future.globals.maxSize=4 * 1024^3)
library(future)
plan(multicore)

source(here("scripts", "utilities.R"))

# Required in order to use DGEList objects with future
length.DGEList <- function(x) {
    length(unclass(x))
}

We also set the random seed for reproducibility:

set.seed(1986)

Data Loading and Preprocessing

First we load the consensus peaks called from the reads pooled from all samples. This consensus peak set is not biased toward or against any sample or condition, and therefore the peak significance is expected to be independent of any differential binding in that peak.

peakfile <- here(
    "peak_calls", "epic_hg38.analysisSet",
    glue("{params$histone_mark}_condition.ALL_donor.ALL"),
    "peaks_noBL_IDR.narrowPeak")
allpeaks <- {
    read.narrowPeak(peakfile) %>% as("GRanges") %>%
        assign_into(seqinfo(.), seqinfo(BSgenome.Hsapiens.UCSC.hg38)[seqlevels(.)]) %>%
        setNames(.$name)
}

Now we’ll load the ChIP-seq read count data set from RDS files containing SummarizedExperiment objects, and modify them to use the sample names as column names. We also ensure that the column order is identical between the two objects. Lastly, we filter out any windows with fewer than one count per sample. This is a very mild filtering criterion, but it often eliminates many windows, greatly easing the subsequent computational burden of computing the real filtering threshold.

sexpfile <-
    here("saved_data",
         glue_data(params, "csaw-counts_{window_size}-windows_{fragment_length}-reads_{histone_mark}.RDS"))
full.sexp <- readRDS(sexpfile)
colnames(full.sexp) <- colData(full.sexp)$SampleName
sexp <- full.sexp %>% .[rowSums(assay(.)) >= ncol(.),]
# Exepected number of counts per read, based on overlapping multiple windows.
# NOTE: Assumes windows exactly tile the genome (no overlaps, no gaps).
colData(sexp)$CountDupFactor <- (colData(sexp)$ext - 1) / median(width(rowRanges(sexp))) + 1

We extract the sample metadata from the SummarizedExperiment. We set all factors to use a sum-to-zero variant of the treatment-contrast coding, which will ease the subtraction of batch effects later.

sample.table <- colData(sexp) %>%
    as.data.frame %>% autoFactorize %>%
    mutate(days_after_activation=time_point %>% str_extract("\\d+$") %>% as.numeric(),
           time_point=factor(days_after_activation) %>% `levels<-`(glue("D{levels(.)}")),
           group=interaction(cell_type, time_point, sep="")) %>%
    autoFactorize %>%
    set_rownames(colnames(sexp))
for (i in names(sample.table)) {
    if (is.factor(sample.table[[i]]) && nlevels(sample.table[[i]]) > 1) {
        sample.table[[i]] %<>% C(code_control_named(levels(.)))
    }
}

Peak and Window Filtering

We first select all peaks with an IDR value of 0.2 or less. This is a quite relaxed threshold in order to ensure that even peaks present in only one condition will be included.

idr.threshold <- 0.2
genome.size <- seqlengths(seqinfo(allpeaks)) %>% as.numeric %>% sum
peaks <- allpeaks[allpeaks$qValue >= -log10(idr.threshold)]
pct.covered <- width(peaks) %>% sum %>% divide_by(genome.size) %>% multiply_by(100)
mean.pct.reads <- sexp %>% subsetByOverlaps(peaks) %>% assay("counts") %>%
    colSums %>% divide_by(colData(sexp) %$% {totals * CountDupFactor}) %>% multiply_by(100) %>%
    mean
message(glue("Selected {length(peaks)} peaks at an IDR threshold of {format(idr.threshold, digits=3)}, with an average width of {round(mean(width(peaks)))} nucleotides and covering a total of {format(pct.covered, digits=3)}% of the genome, containing on average {format(mean.pct.reads, digits=3)}% of reads"))
Selected 21631 peaks at an IDR threshold of 0.2, with an average width of 3512 nucleotides and covering a total of 2.46% of the genome, containing on average 16.6% of reads

Then we select only the windows overlapping the selected peaks.

sexp %<>% subsetByOverlaps(peaks)

Now we create a DGEList from the counts.

## Extract gene metadata and colapse lists
all.window.meta <- rowRanges(sexp) %>% as.data.frame %>%
    select(-width, -strand) %>% rename(chr=seqnames)
# Convert list columns to character vectors
all.window.meta[] %<>% lapply(function(x) if (is.list(x)) sapply(x, str_c, collapse=",") else x)
rownames(all.window.meta) <- glue_data(all.window.meta, "{chr}:{start}-{end}")
dge <- asDGEList(sexp) %>%
    assign_into(.$offset, NULL) %>%
    assign_into(.$genes, all.window.meta) %>%
    set_rownames(rownames(all.window.meta))

Then we select all windows with an average read count of at least 5.

count.threshold <- 5
filter.threshold <- aveLogCPM(count.threshold, lib.size=mean(dge$samples$lib.size))
filt <- aveLogCPM(dge) >= filter.threshold
dge <- dge[filt,]
message(glue("Excluding {sum(filt == FALSE)} out of {length(filt)} peak-overlapping windows ({format(100*(1-mean(filt)), digits=3)}%) with average count below {count.threshold}."))
Excluding 46893 out of 170399 peak-overlapping windows (27.5%) with average count below 5.

Model fitting

Now we are ready to fit a model to the data. We begin by building the design matrix, inclulding coefficients for the interaction of cell type and time point (group).

design <- model.matrix(~0 + group, sample.table, strip.prefixes = TRUE)
colnames(design)
[1] "NaiveD0"   "MemoryD0"  "NaiveD1"   "MemoryD1"  "NaiveD5"   "MemoryD5" 
[7] "NaiveD14"  "MemoryD14"
NaiveD0

MemoryD0

NaiveD1

MemoryD1

NaiveD5

MemoryD5

NaiveD14

MemoryD14

Instead of incorporating all the batch effects we think might be important into the design matrix, we let SVA infer the important confounding factors from the data, and add those to the design matrix.

dge %<>% calcNormFactors()
logcpm <- cpm(dge, log=TRUE, prior.count=1)
svobj <- sva(logcpm, design)
Number of significant surrogate variables is:  8 
Iteration (out of 5 ):1  2  3  4  5  
svmat <- add.numbered.colnames(svobj$sv, "SV")
design.sv <- cbind(design, svmat)

We plot the surrogate variables against known variables and also test for significant correlations between them.

sv.cor.tests <- expand.grid(SV=colnames(svmat),
                            Var=c("cell_type", "time_point", "group", "donor_id", "totals")) %>%
    select(Var, SV) %>%
    group_by_all() %>% 
    do(mod=list(lm(as.formula(glue("{.$SV} ~ {.$Var}")), data=cbind(sample.table, svmat)))) %>%
    mutate(aov=lapply(mod, anova)) %>%
    ungroup() %>%
    mutate(PValue=vapply(.$aov, . %$% {`Pr(>F)`[1]}, numeric(1)),
           FDR=p.adjust(PValue, method="BH")) %>%
    select(-mod, -aov)
sv.cor.tests %>% arrange(PValue)
ggduo.dataXY(sample.table[c("cell_type", "time_point", "donor_id", "totals")], svmat,
             extraData=sample.table["group"],
             types=list(comboVertical="dot_no_facet"),
             mapping=aes(color=group, shape=donor_id))

We now proceed to computing normalization factors, estimating dispersions, and fitting the quasi-likelihood GLM.

dge %<>% estimateDisp(design.sv, robust=TRUE)
plotBCV(dge)

fit <- glmQLFit(dge, design.sv, robust=TRUE)
plotQLDisp(fit)

Next, we define the differential expression tests we wish to perform as contrasts. Each contrast is an arithmetic expression in terms of the model coefficients.

celltypes <- levels(sample.table$cell_type)
all.timepoints <- levels(sample.table$time_point)
nonzero.timepoints <- setdiff(all.timepoints, "D0")

timepoint.anova.tests <- setNames(lapply(celltypes, function(ct) {
    setNames(glue("{ct}{nonzero.timepoints} - {ct}D0"),
             nm=glue("{ct}.D0v{nonzero.timepoints}"))
}), nm=str_c(celltypes, ".AllT"))
timepoint.single.tests <- as.list(unlist(unname(timepoint.anova.tests)))
celltype.singlet.tests <-
    as.list(setNames(glue("Memory{all.timepoints} - Naive{all.timepoints}"),
                     glue("NvM.{all.timepoints}")))
celltype.allt.test <- list(NvM.AllT=unlist(celltype.singlet.tests))
factorial.singlet.tests <-
    as.list(setNames(glue("(Memory{nonzero.timepoints} - MemoryD0) - (Naive{nonzero.timepoints} - NaiveD0)"),
                     glue("Fac.{nonzero.timepoints}")))
factorial.allt.test <- list(Fac.AllT=unlist(factorial.singlet.tests))
mi.vs.nf.test <- list(MD0vND14="MemoryD0 - NaiveD14")
sv.test <- list(SV=colnames(svmat))
alltests <- c(timepoint.anova.tests, timepoint.single.tests,
              celltype.allt.test, celltype.singlet.tests,
              factorial.allt.test, factorial.singlet.tests,
              mi.vs.nf.test,
              sv.test
              ) %>% lapply(as_glue)
print(alltests)
$Naive.AllT
NaiveD1 - NaiveD0
NaiveD5 - NaiveD0
NaiveD14 - NaiveD0

$Memory.AllT
MemoryD1 - MemoryD0
MemoryD5 - MemoryD0
MemoryD14 - MemoryD0

$Naive.D0vD1
NaiveD1 - NaiveD0

$Naive.D0vD5
NaiveD5 - NaiveD0

$Naive.D0vD14
NaiveD14 - NaiveD0

$Memory.D0vD1
MemoryD1 - MemoryD0

$Memory.D0vD5
MemoryD5 - MemoryD0

$Memory.D0vD14
MemoryD14 - MemoryD0

$NvM.AllT
MemoryD0 - NaiveD0
MemoryD1 - NaiveD1
MemoryD5 - NaiveD5
MemoryD14 - NaiveD14

$NvM.D0
MemoryD0 - NaiveD0

$NvM.D1
MemoryD1 - NaiveD1

$NvM.D5
MemoryD5 - NaiveD5

$NvM.D14
MemoryD14 - NaiveD14

$Fac.AllT
(MemoryD1 - MemoryD0) - (NaiveD1 - NaiveD0)
(MemoryD5 - MemoryD0) - (NaiveD5 - NaiveD0)
(MemoryD14 - MemoryD0) - (NaiveD14 - NaiveD0)

$Fac.D1
(MemoryD1 - MemoryD0) - (NaiveD1 - NaiveD0)

$Fac.D5
(MemoryD5 - MemoryD0) - (NaiveD5 - NaiveD0)

$Fac.D14
(MemoryD14 - MemoryD0) - (NaiveD14 - NaiveD0)

$MD0vND14
MemoryD0 - NaiveD14

$SV
SV1
SV2
SV3
SV4
SV5
SV6
SV7
SV8

We now perform the differential modification tests for each contrast or set of contrasts. For a single contrast, the test is analogous to a t-test. For a multi-contrast test, the global null hypothesis that all contrasts are equal to zero is used, analogous to an F-test.

window.results.tables <- bplapply(alltests, function(ct) {
    ctmat <- makeContrasts(contrasts = ct, levels=design.sv) %>% set_colnames(names(ct))
    ctest <- glmQLFTest(fit, contrast = ctmat)
    topTags(ctest, n=Inf, sort.by="none") %>% as.data.frame %>% add.qvalue
})
p <- bplapply(names(window.results.tables), function(testname) {
    pvals <- window.results.tables[[testname]]$PValue
    pi0 <- pi0est(pvals)$pi0
    p <- plotpvals(pvals, ptn = pi0) +
        labs(title=glue("P-value histogram for {testname}"),
             subtitle=glue("Est. Non-Null Prop.: {(format((1 - pi0) * 100, digits=3))}%"))
})
ggprint(p)

Next, because we wish to control the FDR at the level of peaks, not individual windows, we must combine the results for all the windows in each peak. The p-value for each peak tests the combined null hypothesis that no windows in that peak are differentially modified with respect to the specified contrast(s).

peak.results.tables <- bplapply(window.results.tables, function(tt) {
    gr <- as(tt, "GRanges")
    olap <- findOverlaps(peaks, gr)
    combineOverlaps(olap, tt) %>%
        filter(!is.na(PValue)) %>%
        arrange(PValue) %>%
        add.qvalue
})
peak.results.tables %<>% lapply(. %>% add.qvalue)

We now visualize the p-value histogram for each test. Ideally, each p-value histogram should either be uniform or biased toward zero. Any other shape is indicative of a failure of some modelling step.

p <- bplapply(names(peak.results.tables), function(testname) {
    pvals <- peak.results.tables[[testname]]$PValue
    pi0 <- pi0est(pvals)$pi0
    p <- plotpvals(pvals, ptn = pi0) +
        labs(title=glue("P-value histogram for {testname}"),
             subtitle=glue("Est. Non-Null Prop.: {(format((1 - pi0) * 100, digits=3))}%"))
})
ggprint(p)

peak.results.tables %>% lapply(function(rt) {
    summarize(rt,
              NumSignif=FDR %>% is_weakly_less_than(0.1) %>% sum,
              EstNonNull=floor(length(PValue) * (1 - propTrueNull(PValue))))
}) %>% 
    do.call(what=rbind) %>% 
    mutate(Test=rownames(.)) %>%
    select(Test, everything())
# Take all genes significant at 10% FDR or top 100 genes, whichever is more
filtered.peak.results.tables <- lapply(peak.results.tables, . %>% filter(FDR <= 0.1 | seq_len(nrow(.)) <= 100))
dir.create(here("saved_data", "ChIP-seq"), recursive = TRUE, showWarnings = FALSE)
dir.create(here("results", "ChIP-seq"), recursive = TRUE, showWarnings = FALSE)
saveRDS(window.results.tables,
        here('saved_data', 'ChIP-seq',
             glue('{params$histone_mark}-window-diffmod.RDS')))
saveRDS(peak.results.tables,
        here('saved_data', 'ChIP-seq',
             glue('{params$histone_mark}-peak-diffmod.RDS')))
save.image(here('saved_data', 'ChIP-seq',
                glue('{params$histone_mark}-diffmod.rda')))
write.xlsx(filtered.peak.results.tables,
           here("results", "ChIP-seq",
                glue('{params$histone_mark}-peak-diffmod.xlsx')))
LS0tCnRpdGxlOiAiRGlmZmVyZW50aWFsIE1vZGlmaWNhdGlvbiBBbmFseXNpcyBvZiBDRDQgQ2hJUC1TZXEgRGF0YXNldCIKYXV0aG9yOiAiUnlhbiBDLiBUaG9tcHNvbiIKZGF0ZTogJ2ByIGdzdWIoIlxccysiLCAiICIsIGZvcm1hdChTeXMudGltZSgpLCAiJUIgJWUsICVZIikpYCcKb3V0cHV0OgogIGh0bWxfZG9jdW1lbnQ6IGRlZmF1bHQKICBodG1sX25vdGVib29rOiBkZWZhdWx0CnN1YnRpdGxlOiAnYHIgcGFzdGUwKCJGb3IgaGlzdG9uZSBtYXJrICIsIHBhcmFtcyRoaXN0b25lX21hcmspYCcKcGFyYW1zOgogIGhpc3RvbmVfbWFyazoKICAgIHZhbHVlOiBIM0s0bWUzCiAgd2luZG93X3NpemU6CiAgICB2YWx1ZTogNTAwYnAKICBmcmFnbWVudF9sZW5ndGg6CiAgICB2YWx1ZTogMTQ3YnAKLS0tCgojIFByZWxpbWluYXJ5IFNldHVwCgpgYGB7ciBzZXR1cCwgaW5jbHVkZT1GQUxTRSwgY2FjaGU9RkFMU0V9CmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvID0gVFJVRSwgcmV0aW5hPTIsIGNhY2hlPVRSVUUsIGF1dG9kZXA9VFJVRSwKICAgICAgICAgICAgICAgICAgICAgIGNhY2hlLmV4dHJhID0gbGlzdChwYXJhbXM9cGFyYW1zKSwKICAgICAgICAgICAgICAgICAgICAgICMgaHR0cHM6Ly9naXRodWIuY29tL3lpaHVpL2tuaXRyL2lzc3Vlcy81NzIKICAgICAgICAgICAgICAgICAgICAgIGNhY2hlLmxhenk9RkFMU0UsCiAgICAgICAgICAgICAgICAgICAgICBmaWcuaGVpZ2h0PTgsIGZpZy53aWR0aD04LAogICAgICAgICAgICAgICAgICAgICAgY2FjaGUucGF0aCA9IHBhc3RlMCgKICAgICAgICAgICAgICAgICAgICAgICAgICBoZXJlOjpoZXJlKCJjYWNoZSIsICJjaGlwc2VxLWRpZmZtb2QiLCBwYXJhbXMkaGlzdG9uZV9tYXJrKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAuUGxhdGZvcm0kZmlsZS5zZXApKQpgYGAKCkZpcnN0IHdlIGxvYWQgdGhlIG5lY2Vzc2FyeSBsaWJyYXJpZXMsIGFsb25nIHdpdGggYSBzZXQgb2YgdXRpbGl0eSBmdW5jdGlvbnMuCgpgYGB7ciBsb2FkX3BhY2thZ2VzLCBtZXNzYWdlPUZBTFNFLCBjYWNoZT1GQUxTRX0KbGlicmFyeShoZXJlKQpsaWJyYXJ5KHN0cmluZ3IpCmxpYnJhcnkoZ2x1ZSkKbGlicmFyeShtYWdyaXR0cikKbGlicmFyeShvcGVueGxzeCkKbGlicmFyeShTdW1tYXJpemVkRXhwZXJpbWVudCkKbGlicmFyeShkcGx5cikKbGlicmFyeShlZGdlUikKbGlicmFyeShsaW1tYSkKbGlicmFyeShjc2F3KQpsaWJyYXJ5KHN2YSkKbGlicmFyeShnZ3Bsb3QyKQpsaWJyYXJ5KHNjYWxlcykKbGlicmFyeShHR2FsbHkpCmxpYnJhcnkoZ2dhbHQpCmxpYnJhcnkoZ2d0aGVtZXMpCmxpYnJhcnkoc3BsaW5lcykKbGlicmFyeShyZXNoYXBlMikKbGlicmFyeShhc3NlcnR0aGF0KQpsaWJyYXJ5KGdnZm9ydGlmeSkKbGlicmFyeShicm9vbSkKbGlicmFyeShrcykKbGlicmFyeShSQ29sb3JCcmV3ZXIpCgpsaWJyYXJ5KEJTZ2Vub21lLkhzYXBpZW5zLlVDU0MuaGczOCkKCmxpYnJhcnkoZG9QYXJhbGxlbCkKbmNvcmVzIDwtIGdldE9wdGlvbigibWMuY29yZXMiLCBkZWZhdWx0PXBhcmFsbGVsOjpkZXRlY3RDb3Jlcyhsb2dpY2FsID0gRkFMU0UpKQpvcHRpb25zKG1jLmNvcmVzPW5jb3JlcykKcmVnaXN0ZXJEb1BhcmFsbGVsKGNvcmVzPW5jb3JlcykKbGlicmFyeShCaW9jUGFyYWxsZWwpCnJlZ2lzdGVyKERvcGFyUGFyYW0oKSkKCm9wdGlvbnMoZnV0dXJlLmdsb2JhbHMubWF4U2l6ZT00ICogMTAyNF4zKQpsaWJyYXJ5KGZ1dHVyZSkKcGxhbihtdWx0aWNvcmUpCgpzb3VyY2UoaGVyZSgic2NyaXB0cyIsICJ1dGlsaXRpZXMuUiIpKQoKIyBSZXF1aXJlZCBpbiBvcmRlciB0byB1c2UgREdFTGlzdCBvYmplY3RzIHdpdGggZnV0dXJlCmxlbmd0aC5ER0VMaXN0IDwtIGZ1bmN0aW9uKHgpIHsKICAgIGxlbmd0aCh1bmNsYXNzKHgpKQp9CmBgYAoKV2UgYWxzbyBzZXQgdGhlIHJhbmRvbSBzZWVkIGZvciByZXByb2R1Y2liaWxpdHk6CgpgYGB7ciBzZXRfcmFuZG9tX3NlZWR9CnNldC5zZWVkKDE5ODYpCmBgYAoKIyBEYXRhIExvYWRpbmcgYW5kIFByZXByb2Nlc3NpbmcKCkZpcnN0IHdlIGxvYWQgdGhlIGNvbnNlbnN1cyBwZWFrcyBjYWxsZWQgZnJvbSB0aGUgcmVhZHMgcG9vbGVkIGZyb20gYWxsIHNhbXBsZXMuIFRoaXMgY29uc2Vuc3VzIHBlYWsgc2V0IGlzIG5vdCBiaWFzZWQgdG93YXJkIG9yIGFnYWluc3QgYW55IHNhbXBsZSBvciBjb25kaXRpb24sIGFuZCB0aGVyZWZvcmUgdGhlIHBlYWsgc2lnbmlmaWNhbmNlIGlzIGV4cGVjdGVkIHRvIGJlIGluZGVwZW5kZW50IG9mIGFueSBkaWZmZXJlbnRpYWwgYmluZGluZyBpbiB0aGF0IHBlYWsuCgpgYGB7ciBsb2FkX3BlYWtzfQpwZWFrZmlsZSA8LSBoZXJlKAogICAgInBlYWtfY2FsbHMiLCAiZXBpY19oZzM4LmFuYWx5c2lzU2V0IiwKICAgIGdsdWUoIntwYXJhbXMkaGlzdG9uZV9tYXJrfV9jb25kaXRpb24uQUxMX2Rvbm9yLkFMTCIpLAogICAgInBlYWtzX25vQkxfSURSLm5hcnJvd1BlYWsiKQphbGxwZWFrcyA8LSB7CiAgICByZWFkLm5hcnJvd1BlYWsocGVha2ZpbGUpICU+JSBhcygiR1JhbmdlcyIpICU+JQogICAgICAgIGFzc2lnbl9pbnRvKHNlcWluZm8oLiksIHNlcWluZm8oQlNnZW5vbWUuSHNhcGllbnMuVUNTQy5oZzM4KVtzZXFsZXZlbHMoLildKSAlPiUKICAgICAgICBzZXROYW1lcyguJG5hbWUpCn0KYGBgCgpOb3cgd2UnbGwgbG9hZCB0aGUgQ2hJUC1zZXEgcmVhZCBjb3VudCBkYXRhIHNldCBmcm9tIFJEUyBmaWxlcyBjb250YWluaW5nIFN1bW1hcml6ZWRFeHBlcmltZW50IG9iamVjdHMsIGFuZCBtb2RpZnkgdGhlbSB0byB1c2UgdGhlIHNhbXBsZSBuYW1lcyBhcyBjb2x1bW4gbmFtZXMuIFdlIGFsc28gZW5zdXJlIHRoYXQgdGhlIGNvbHVtbiBvcmRlciBpcyBpZGVudGljYWwgYmV0d2VlbiB0aGUgdHdvIG9iamVjdHMuIExhc3RseSwgd2UgZmlsdGVyIG91dCBhbnkgd2luZG93cyB3aXRoIGZld2VyIHRoYW4gb25lIGNvdW50IHBlciBzYW1wbGUuIFRoaXMgaXMgYSB2ZXJ5IG1pbGQgZmlsdGVyaW5nIGNyaXRlcmlvbiwgYnV0IGl0IG9mdGVuIGVsaW1pbmF0ZXMgbWFueSB3aW5kb3dzLCBncmVhdGx5IGVhc2luZyB0aGUgc3Vic2VxdWVudCBjb21wdXRhdGlvbmFsIGJ1cmRlbiBvZiBjb21wdXRpbmcgdGhlICpyZWFsKiBmaWx0ZXJpbmcgdGhyZXNob2xkLgoKYGBge3IgbG9hZF9jb3VudHN9CnNleHBmaWxlIDwtCiAgICBoZXJlKCJzYXZlZF9kYXRhIiwKICAgICAgICAgZ2x1ZV9kYXRhKHBhcmFtcywgImNzYXctY291bnRzX3t3aW5kb3dfc2l6ZX0td2luZG93c197ZnJhZ21lbnRfbGVuZ3RofS1yZWFkc197aGlzdG9uZV9tYXJrfS5SRFMiKSkKZnVsbC5zZXhwIDwtIHJlYWRSRFMoc2V4cGZpbGUpCmNvbG5hbWVzKGZ1bGwuc2V4cCkgPC0gY29sRGF0YShmdWxsLnNleHApJFNhbXBsZU5hbWUKc2V4cCA8LSBmdWxsLnNleHAgJT4lIC5bcm93U3Vtcyhhc3NheSguKSkgPj0gbmNvbCguKSxdCiMgRXhlcGVjdGVkIG51bWJlciBvZiBjb3VudHMgcGVyIHJlYWQsIGJhc2VkIG9uIG92ZXJsYXBwaW5nIG11bHRpcGxlIHdpbmRvd3MuCiMgTk9URTogQXNzdW1lcyB3aW5kb3dzIGV4YWN0bHkgdGlsZSB0aGUgZ2Vub21lIChubyBvdmVybGFwcywgbm8gZ2FwcykuCmNvbERhdGEoc2V4cCkkQ291bnREdXBGYWN0b3IgPC0gKGNvbERhdGEoc2V4cCkkZXh0IC0gMSkgLyBtZWRpYW4od2lkdGgocm93UmFuZ2VzKHNleHApKSkgKyAxCmBgYAoKV2UgZXh0cmFjdCB0aGUgc2FtcGxlIG1ldGFkYXRhIGZyb20gdGhlIFN1bW1hcml6ZWRFeHBlcmltZW50LiBXZSBzZXQgYWxsIGZhY3RvcnMgdG8gdXNlIGEgc3VtLXRvLXplcm8gdmFyaWFudCBvZiB0aGUgdHJlYXRtZW50LWNvbnRyYXN0IGNvZGluZywgd2hpY2ggd2lsbCBlYXNlIHRoZSBzdWJ0cmFjdGlvbiBvZiBiYXRjaCBlZmZlY3RzIGxhdGVyLgoKYGBge3IgZXh0cmFjdF9zYW1wbGVtZXRhfQpzYW1wbGUudGFibGUgPC0gY29sRGF0YShzZXhwKSAlPiUKICAgIGFzLmRhdGEuZnJhbWUgJT4lIGF1dG9GYWN0b3JpemUgJT4lCiAgICBtdXRhdGUoZGF5c19hZnRlcl9hY3RpdmF0aW9uPXRpbWVfcG9pbnQgJT4lIHN0cl9leHRyYWN0KCJcXGQrJCIpICU+JSBhcy5udW1lcmljKCksCiAgICAgICAgICAgdGltZV9wb2ludD1mYWN0b3IoZGF5c19hZnRlcl9hY3RpdmF0aW9uKSAlPiUgYGxldmVsczwtYChnbHVlKCJEe2xldmVscyguKX0iKSksCiAgICAgICAgICAgZ3JvdXA9aW50ZXJhY3Rpb24oY2VsbF90eXBlLCB0aW1lX3BvaW50LCBzZXA9IiIpKSAlPiUKICAgIGF1dG9GYWN0b3JpemUgJT4lCiAgICBzZXRfcm93bmFtZXMoY29sbmFtZXMoc2V4cCkpCmZvciAoaSBpbiBuYW1lcyhzYW1wbGUudGFibGUpKSB7CiAgICBpZiAoaXMuZmFjdG9yKHNhbXBsZS50YWJsZVtbaV1dKSAmJiBubGV2ZWxzKHNhbXBsZS50YWJsZVtbaV1dKSA+IDEpIHsKICAgICAgICBzYW1wbGUudGFibGVbW2ldXSAlPD4lIEMoY29kZV9jb250cm9sX25hbWVkKGxldmVscyguKSkpCiAgICB9Cn0KYGBgCgojIFBlYWsgYW5kIFdpbmRvdyBGaWx0ZXJpbmcKCldlIGZpcnN0IHNlbGVjdCBhbGwgcGVha3MgIHdpdGggYW4gSURSIHZhbHVlIG9mIDAuMiBvciBsZXNzLiBUaGlzIGlzIGEgcXVpdGUgcmVsYXhlZCB0aHJlc2hvbGQgaW4gb3JkZXIgdG8gZW5zdXJlIHRoYXQgZXZlbiBwZWFrcyBwcmVzZW50IGluIG9ubHkgb25lIGNvbmRpdGlvbiB3aWxsIGJlIGluY2x1ZGVkLgoKYGBge3IgZmlsdGVyX3BlYWtzfQppZHIudGhyZXNob2xkIDwtIDAuMgpnZW5vbWUuc2l6ZSA8LSBzZXFsZW5ndGhzKHNlcWluZm8oYWxscGVha3MpKSAlPiUgYXMubnVtZXJpYyAlPiUgc3VtCnBlYWtzIDwtIGFsbHBlYWtzW2FsbHBlYWtzJHFWYWx1ZSA+PSAtbG9nMTAoaWRyLnRocmVzaG9sZCldCnBjdC5jb3ZlcmVkIDwtIHdpZHRoKHBlYWtzKSAlPiUgc3VtICU+JSBkaXZpZGVfYnkoZ2Vub21lLnNpemUpICU+JSBtdWx0aXBseV9ieSgxMDApCm1lYW4ucGN0LnJlYWRzIDwtIHNleHAgJT4lIHN1YnNldEJ5T3ZlcmxhcHMocGVha3MpICU+JSBhc3NheSgiY291bnRzIikgJT4lCiAgICBjb2xTdW1zICU+JSBkaXZpZGVfYnkoY29sRGF0YShzZXhwKSAlJCUge3RvdGFscyAqIENvdW50RHVwRmFjdG9yfSkgJT4lIG11bHRpcGx5X2J5KDEwMCkgJT4lCiAgICBtZWFuCm1lc3NhZ2UoZ2x1ZSgiU2VsZWN0ZWQge2xlbmd0aChwZWFrcyl9IHBlYWtzIGF0IGFuIElEUiB0aHJlc2hvbGQgb2Yge2Zvcm1hdChpZHIudGhyZXNob2xkLCBkaWdpdHM9Myl9LCB3aXRoIGFuIGF2ZXJhZ2Ugd2lkdGggb2Yge3JvdW5kKG1lYW4od2lkdGgocGVha3MpKSl9IG51Y2xlb3RpZGVzIGFuZCBjb3ZlcmluZyBhIHRvdGFsIG9mIHtmb3JtYXQocGN0LmNvdmVyZWQsIGRpZ2l0cz0zKX0lIG9mIHRoZSBnZW5vbWUsIGNvbnRhaW5pbmcgb24gYXZlcmFnZSB7Zm9ybWF0KG1lYW4ucGN0LnJlYWRzLCBkaWdpdHM9Myl9JSBvZiByZWFkcyIpKQpgYGAKClRoZW4gd2Ugc2VsZWN0IG9ubHkgdGhlIHdpbmRvd3Mgb3ZlcmxhcHBpbmcgdGhlIHNlbGVjdGVkIHBlYWtzLgoKYGBge3IgZmlsdGVyX3dpbmRvd3NfYnlfcGVha19vdmVybGFwfQpzZXhwICU8PiUgc3Vic2V0QnlPdmVybGFwcyhwZWFrcykKYGBgCgpOb3cgd2UgY3JlYXRlIGEgREdFTGlzdCBmcm9tIHRoZSBjb3VudHMuCgpgYGB7ciBwcmVwYXJlX2RnZWxpc3R9CiMjIEV4dHJhY3QgZ2VuZSBtZXRhZGF0YSBhbmQgY29sYXBzZSBsaXN0cwphbGwud2luZG93Lm1ldGEgPC0gcm93UmFuZ2VzKHNleHApICU+JSBhcy5kYXRhLmZyYW1lICU+JQogICAgc2VsZWN0KC13aWR0aCwgLXN0cmFuZCkgJT4lIHJlbmFtZShjaHI9c2VxbmFtZXMpCiMgQ29udmVydCBsaXN0IGNvbHVtbnMgdG8gY2hhcmFjdGVyIHZlY3RvcnMKYWxsLndpbmRvdy5tZXRhW10gJTw+JSBsYXBwbHkoZnVuY3Rpb24oeCkgaWYgKGlzLmxpc3QoeCkpIHNhcHBseSh4LCBzdHJfYywgY29sbGFwc2U9IiwiKSBlbHNlIHgpCnJvd25hbWVzKGFsbC53aW5kb3cubWV0YSkgPC0gZ2x1ZV9kYXRhKGFsbC53aW5kb3cubWV0YSwgIntjaHJ9OntzdGFydH0te2VuZH0iKQpkZ2UgPC0gYXNER0VMaXN0KHNleHApICU+JQogICAgYXNzaWduX2ludG8oLiRvZmZzZXQsIE5VTEwpICU+JQogICAgYXNzaWduX2ludG8oLiRnZW5lcywgYWxsLndpbmRvdy5tZXRhKSAlPiUKICAgIHNldF9yb3duYW1lcyhyb3duYW1lcyhhbGwud2luZG93Lm1ldGEpKQpgYGAKClRoZW4gd2Ugc2VsZWN0IGFsbCB3aW5kb3dzIHdpdGggYW4gYXZlcmFnZSByZWFkIGNvdW50IG9mIGF0IGxlYXN0IDUuCgpgYGB7ciBzZWxlY3RfYWJ1bmRhbnRfd2luZG93c30KY291bnQudGhyZXNob2xkIDwtIDUKZmlsdGVyLnRocmVzaG9sZCA8LSBhdmVMb2dDUE0oY291bnQudGhyZXNob2xkLCBsaWIuc2l6ZT1tZWFuKGRnZSRzYW1wbGVzJGxpYi5zaXplKSkKZmlsdCA8LSBhdmVMb2dDUE0oZGdlKSA+PSBmaWx0ZXIudGhyZXNob2xkCmRnZSA8LSBkZ2VbZmlsdCxdCm1lc3NhZ2UoZ2x1ZSgiRXhjbHVkaW5nIHtzdW0oZmlsdCA9PSBGQUxTRSl9IG91dCBvZiB7bGVuZ3RoKGZpbHQpfSBwZWFrLW92ZXJsYXBwaW5nIHdpbmRvd3MgKHtmb3JtYXQoMTAwKigxLW1lYW4oZmlsdCkpLCBkaWdpdHM9Myl9JSkgd2l0aCBhdmVyYWdlIGNvdW50IGJlbG93IHtjb3VudC50aHJlc2hvbGR9LiIpKQpgYGAKCiMgTW9kZWwgZml0dGluZwoKTm93IHdlIGFyZSByZWFkeSB0byBmaXQgYSBtb2RlbCB0byB0aGUgZGF0YS4gV2UgYmVnaW4gYnkgYnVpbGRpbmcgdGhlIGRlc2lnbiBtYXRyaXgsIGluY2x1bGRpbmcgY29lZmZpY2llbnRzIGZvciB0aGUgaW50ZXJhY3Rpb24gb2YgY2VsbCB0eXBlIGFuZCB0aW1lIHBvaW50IChncm91cCkuCgpgYGB7ciBidWlsZF9kZXNpZ25fbWF0cml4fQpkZXNpZ24gPC0gbW9kZWwubWF0cml4KH4wICsgZ3JvdXAsIHNhbXBsZS50YWJsZSwgc3RyaXAucHJlZml4ZXMgPSBUUlVFKQpjb2xuYW1lcyhkZXNpZ24pCmBgYAoKSW5zdGVhZCBvZiBpbmNvcnBvcmF0aW5nIGFsbCB0aGUgYmF0Y2ggZWZmZWN0cyB3ZSB0aGluayBtaWdodCBiZSBpbXBvcnRhbnQgaW50byB0aGUgZGVzaWduIG1hdHJpeCwgd2UgbGV0IFNWQSBpbmZlciB0aGUgaW1wb3J0YW50IGNvbmZvdW5kaW5nIGZhY3RvcnMgZnJvbSB0aGUgZGF0YSwgYW5kIGFkZCB0aG9zZSB0byB0aGUgZGVzaWduIG1hdHJpeC4KCmBgYHtyIHN2YX0KZGdlICU8PiUgY2FsY05vcm1GYWN0b3JzKCkKbG9nY3BtIDwtIGNwbShkZ2UsIGxvZz1UUlVFLCBwcmlvci5jb3VudD0xKQpzdm9iaiA8LSBzdmEobG9nY3BtLCBkZXNpZ24pCnN2bWF0IDwtIGFkZC5udW1iZXJlZC5jb2xuYW1lcyhzdm9iaiRzdiwgIlNWIikKZGVzaWduLnN2IDwtIGNiaW5kKGRlc2lnbiwgc3ZtYXQpCmBgYAoKV2UgcGxvdCB0aGUgc3Vycm9nYXRlIHZhcmlhYmxlcyBhZ2FpbnN0IGtub3duIHZhcmlhYmxlcyBhbmQgYWxzbyB0ZXN0IGZvciBzaWduaWZpY2FudCBjb3JyZWxhdGlvbnMgYmV0d2VlbiB0aGVtLgoKYGBge3IgdGVzdF9zdl9jb3JyfQpzdi5jb3IudGVzdHMgPC0gZXhwYW5kLmdyaWQoU1Y9Y29sbmFtZXMoc3ZtYXQpLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgVmFyPWMoImNlbGxfdHlwZSIsICJ0aW1lX3BvaW50IiwgImdyb3VwIiwgImRvbm9yX2lkIiwgInRvdGFscyIpKSAlPiUKICAgIHNlbGVjdChWYXIsIFNWKSAlPiUKICAgIGdyb3VwX2J5X2FsbCgpICU+JSAKICAgIGRvKG1vZD1saXN0KGxtKGFzLmZvcm11bGEoZ2x1ZSgiey4kU1Z9IH4gey4kVmFyfSIpKSwgZGF0YT1jYmluZChzYW1wbGUudGFibGUsIHN2bWF0KSkpKSAlPiUKICAgIG11dGF0ZShhb3Y9bGFwcGx5KG1vZCwgYW5vdmEpKSAlPiUKICAgIHVuZ3JvdXAoKSAlPiUKICAgIG11dGF0ZShQVmFsdWU9dmFwcGx5KC4kYW92LCAuICUkJSB7YFByKD5GKWBbMV19LCBudW1lcmljKDEpKSwKICAgICAgICAgICBGRFI9cC5hZGp1c3QoUFZhbHVlLCBtZXRob2Q9IkJIIikpICU+JQogICAgc2VsZWN0KC1tb2QsIC1hb3YpCnN2LmNvci50ZXN0cyAlPiUgYXJyYW5nZShQVmFsdWUpCmdnZHVvLmRhdGFYWShzYW1wbGUudGFibGVbYygiY2VsbF90eXBlIiwgInRpbWVfcG9pbnQiLCAiZG9ub3JfaWQiLCAidG90YWxzIildLCBzdm1hdCwKICAgICAgICAgICAgIGV4dHJhRGF0YT1zYW1wbGUudGFibGVbImdyb3VwIl0sCiAgICAgICAgICAgICB0eXBlcz1saXN0KGNvbWJvVmVydGljYWw9ImRvdF9ub19mYWNldCIpLAogICAgICAgICAgICAgbWFwcGluZz1hZXMoY29sb3I9Z3JvdXAsIHNoYXBlPWRvbm9yX2lkKSkKYGBgCldlIG5vdyBwcm9jZWVkIHRvIGNvbXB1dGluZyBub3JtYWxpemF0aW9uIGZhY3RvcnMsIGVzdGltYXRpbmcgZGlzcGVyc2lvbnMsIGFuZCBmaXR0aW5nIHRoZSBxdWFzaS1saWtlbGlob29kIEdMTS4KCmBgYHtyIGZpdF9tb2RlbH0KZGdlICU8PiUgZXN0aW1hdGVEaXNwKGRlc2lnbi5zdiwgcm9idXN0PVRSVUUpCnBsb3RCQ1YoZGdlKQpmaXQgPC0gZ2xtUUxGaXQoZGdlLCBkZXNpZ24uc3YsIHJvYnVzdD1UUlVFKQpwbG90UUxEaXNwKGZpdCkKYGBgCgpOZXh0LCB3ZSBkZWZpbmUgdGhlIGRpZmZlcmVudGlhbCBleHByZXNzaW9uIHRlc3RzIHdlIHdpc2ggdG8gcGVyZm9ybSBhcyBjb250cmFzdHMuIEVhY2ggY29udHJhc3QgaXMgYW4gYXJpdGhtZXRpYyBleHByZXNzaW9uIGluIHRlcm1zIG9mIHRoZSBtb2RlbCBjb2VmZmljaWVudHMuCgpgYGB7ciBkZWZpbmVfY29udHJhc3RzfQpjZWxsdHlwZXMgPC0gbGV2ZWxzKHNhbXBsZS50YWJsZSRjZWxsX3R5cGUpCmFsbC50aW1lcG9pbnRzIDwtIGxldmVscyhzYW1wbGUudGFibGUkdGltZV9wb2ludCkKbm9uemVyby50aW1lcG9pbnRzIDwtIHNldGRpZmYoYWxsLnRpbWVwb2ludHMsICJEMCIpCgp0aW1lcG9pbnQuYW5vdmEudGVzdHMgPC0gc2V0TmFtZXMobGFwcGx5KGNlbGx0eXBlcywgZnVuY3Rpb24oY3QpIHsKICAgIHNldE5hbWVzKGdsdWUoIntjdH17bm9uemVyby50aW1lcG9pbnRzfSAtIHtjdH1EMCIpLAogICAgICAgICAgICAgbm09Z2x1ZSgie2N0fS5EMHZ7bm9uemVyby50aW1lcG9pbnRzfSIpKQp9KSwgbm09c3RyX2MoY2VsbHR5cGVzLCAiLkFsbFQiKSkKdGltZXBvaW50LnNpbmdsZS50ZXN0cyA8LSBhcy5saXN0KHVubGlzdCh1bm5hbWUodGltZXBvaW50LmFub3ZhLnRlc3RzKSkpCmNlbGx0eXBlLnNpbmdsZXQudGVzdHMgPC0KICAgIGFzLmxpc3Qoc2V0TmFtZXMoZ2x1ZSgiTWVtb3J5e2FsbC50aW1lcG9pbnRzfSAtIE5haXZle2FsbC50aW1lcG9pbnRzfSIpLAogICAgICAgICAgICAgICAgICAgICBnbHVlKCJOdk0ue2FsbC50aW1lcG9pbnRzfSIpKSkKY2VsbHR5cGUuYWxsdC50ZXN0IDwtIGxpc3QoTnZNLkFsbFQ9dW5saXN0KGNlbGx0eXBlLnNpbmdsZXQudGVzdHMpKQpmYWN0b3JpYWwuc2luZ2xldC50ZXN0cyA8LQogICAgYXMubGlzdChzZXROYW1lcyhnbHVlKCIoTWVtb3J5e25vbnplcm8udGltZXBvaW50c30gLSBNZW1vcnlEMCkgLSAoTmFpdmV7bm9uemVyby50aW1lcG9pbnRzfSAtIE5haXZlRDApIiksCiAgICAgICAgICAgICAgICAgICAgIGdsdWUoIkZhYy57bm9uemVyby50aW1lcG9pbnRzfSIpKSkKZmFjdG9yaWFsLmFsbHQudGVzdCA8LSBsaXN0KEZhYy5BbGxUPXVubGlzdChmYWN0b3JpYWwuc2luZ2xldC50ZXN0cykpCm1pLnZzLm5mLnRlc3QgPC0gbGlzdChNRDB2TkQxND0iTWVtb3J5RDAgLSBOYWl2ZUQxNCIpCnN2LnRlc3QgPC0gbGlzdChTVj1jb2xuYW1lcyhzdm1hdCkpCmFsbHRlc3RzIDwtIGModGltZXBvaW50LmFub3ZhLnRlc3RzLCB0aW1lcG9pbnQuc2luZ2xlLnRlc3RzLAogICAgICAgICAgICAgIGNlbGx0eXBlLmFsbHQudGVzdCwgY2VsbHR5cGUuc2luZ2xldC50ZXN0cywKICAgICAgICAgICAgICBmYWN0b3JpYWwuYWxsdC50ZXN0LCBmYWN0b3JpYWwuc2luZ2xldC50ZXN0cywKICAgICAgICAgICAgICBtaS52cy5uZi50ZXN0LAogICAgICAgICAgICAgIHN2LnRlc3QKICAgICAgICAgICAgICApICU+JSBsYXBwbHkoYXNfZ2x1ZSkKcHJpbnQoYWxsdGVzdHMpCmBgYAoKV2Ugbm93IHBlcmZvcm0gdGhlIGRpZmZlcmVudGlhbCBtb2RpZmljYXRpb24gdGVzdHMgZm9yIGVhY2ggY29udHJhc3Qgb3Igc2V0IG9mIGNvbnRyYXN0cy4gRm9yIGEgc2luZ2xlIGNvbnRyYXN0LCB0aGUgdGVzdCBpcyBhbmFsb2dvdXMgdG8gYSB0LXRlc3QuIEZvciBhIG11bHRpLWNvbnRyYXN0IHRlc3QsIHRoZSBnbG9iYWwgbnVsbCBoeXBvdGhlc2lzIHRoYXQgYWxsIGNvbnRyYXN0cyBhcmUgZXF1YWwgdG8gemVybyBpcyB1c2VkLCBhbmFsb2dvdXMgdG8gYW4gRi10ZXN0LgoKYGBge3IgdGVzdF9jb250cmFzdHN9CndpbmRvdy5yZXN1bHRzLnRhYmxlcyA8LSBicGxhcHBseShhbGx0ZXN0cywgZnVuY3Rpb24oY3QpIHsKICAgIGN0bWF0IDwtIG1ha2VDb250cmFzdHMoY29udHJhc3RzID0gY3QsIGxldmVscz1kZXNpZ24uc3YpICU+JSBzZXRfY29sbmFtZXMobmFtZXMoY3QpKQogICAgY3Rlc3QgPC0gZ2xtUUxGVGVzdChmaXQsIGNvbnRyYXN0ID0gY3RtYXQpCiAgICB0b3BUYWdzKGN0ZXN0LCBuPUluZiwgc29ydC5ieT0ibm9uZSIpICU+JSBhcy5kYXRhLmZyYW1lICU+JSBhZGQucXZhbHVlCn0pCmBgYAoKYGBge3Igd2luZG93X3B2YWxfaGlzdH0KcCA8LSBicGxhcHBseShuYW1lcyh3aW5kb3cucmVzdWx0cy50YWJsZXMpLCBmdW5jdGlvbih0ZXN0bmFtZSkgewogICAgcHZhbHMgPC0gd2luZG93LnJlc3VsdHMudGFibGVzW1t0ZXN0bmFtZV1dJFBWYWx1ZQogICAgcGkwIDwtIHBpMGVzdChwdmFscykkcGkwCiAgICBwIDwtIHBsb3RwdmFscyhwdmFscywgcHRuID0gcGkwKSArCiAgICAgICAgbGFicyh0aXRsZT1nbHVlKCJQLXZhbHVlIGhpc3RvZ3JhbSBmb3Ige3Rlc3RuYW1lfSIpLAogICAgICAgICAgICAgc3VidGl0bGU9Z2x1ZSgiRXN0LiBOb24tTnVsbCBQcm9wLjogeyhmb3JtYXQoKDEgLSBwaTApICogMTAwLCBkaWdpdHM9MykpfSUiKSkKfSkKZ2dwcmludChwKQpgYGAKCk5leHQsIGJlY2F1c2Ugd2Ugd2lzaCB0byBjb250cm9sIHRoZSBGRFIgYXQgdGhlIGxldmVsIG9mIHBlYWtzLCBub3QgaW5kaXZpZHVhbCB3aW5kb3dzLCB3ZSBtdXN0IGNvbWJpbmUgdGhlIHJlc3VsdHMgZm9yIGFsbCB0aGUgd2luZG93cyBpbiBlYWNoIHBlYWsuIFRoZSBwLXZhbHVlIGZvciBlYWNoIHBlYWsgdGVzdHMgdGhlIGNvbWJpbmVkIG51bGwgaHlwb3RoZXNpcyB0aGF0IG5vIHdpbmRvd3MgaW4gdGhhdCBwZWFrIGFyZSBkaWZmZXJlbnRpYWxseSBtb2RpZmllZCB3aXRoIHJlc3BlY3QgdG8gdGhlIHNwZWNpZmllZCBjb250cmFzdChzKS4KCmBgYHtyIGNsdXN0ZXJfd2luZG93c30KcGVhay5yZXN1bHRzLnRhYmxlcyA8LSBicGxhcHBseSh3aW5kb3cucmVzdWx0cy50YWJsZXMsIGZ1bmN0aW9uKHR0KSB7CiAgICBnciA8LSBhcyh0dCwgIkdSYW5nZXMiKQogICAgb2xhcCA8LSBmaW5kT3ZlcmxhcHMocGVha3MsIGdyKQogICAgY29tYmluZU92ZXJsYXBzKG9sYXAsIHR0KSAlPiUKICAgICAgICBmaWx0ZXIoIWlzLm5hKFBWYWx1ZSkpICU+JQogICAgICAgIGFycmFuZ2UoUFZhbHVlKSAlPiUKICAgICAgICBhZGQucXZhbHVlCn0pCnBlYWsucmVzdWx0cy50YWJsZXMgJTw+JSBsYXBwbHkoLiAlPiUgYWRkLnF2YWx1ZSkKYGBgCgpXZSBub3cgdmlzdWFsaXplIHRoZSBwLXZhbHVlIGhpc3RvZ3JhbSBmb3IgZWFjaCB0ZXN0LiBJZGVhbGx5LCBlYWNoIHAtdmFsdWUgaGlzdG9ncmFtIHNob3VsZCBlaXRoZXIgYmUgdW5pZm9ybSBvciBiaWFzZWQgdG93YXJkIHplcm8uIEFueSBvdGhlciBzaGFwZSBpcyBpbmRpY2F0aXZlIG9mIGEgZmFpbHVyZSBvZiBzb21lIG1vZGVsbGluZyBzdGVwLiAKCmBgYHtyIHBlYWtfcHZhbF9oaXN0fQpwIDwtIGJwbGFwcGx5KG5hbWVzKHBlYWsucmVzdWx0cy50YWJsZXMpLCBmdW5jdGlvbih0ZXN0bmFtZSkgewogICAgcHZhbHMgPC0gcGVhay5yZXN1bHRzLnRhYmxlc1tbdGVzdG5hbWVdXSRQVmFsdWUKICAgIHBpMCA8LSBwaTBlc3QocHZhbHMpJHBpMAogICAgcCA8LSBwbG90cHZhbHMocHZhbHMsIHB0biA9IHBpMCkgKwogICAgICAgIGxhYnModGl0bGU9Z2x1ZSgiUC12YWx1ZSBoaXN0b2dyYW0gZm9yIHt0ZXN0bmFtZX0iKSwKICAgICAgICAgICAgIHN1YnRpdGxlPWdsdWUoIkVzdC4gTm9uLU51bGwgUHJvcC46IHsoZm9ybWF0KCgxIC0gcGkwKSAqIDEwMCwgZGlnaXRzPTMpKX0lIikpCn0pCmdncHJpbnQocCkKYGBgCgpgYGB7ciBudW1fc2lnbmlmfQpwZWFrLnJlc3VsdHMudGFibGVzICU+JSBsYXBwbHkoZnVuY3Rpb24ocnQpIHsKICAgIHN1bW1hcml6ZShydCwKICAgICAgICAgICAgICBOdW1TaWduaWY9RkRSICU+JSBpc193ZWFrbHlfbGVzc190aGFuKDAuMSkgJT4lIHN1bSwKICAgICAgICAgICAgICBFc3ROb25OdWxsPWZsb29yKGxlbmd0aChQVmFsdWUpICogKDEgLSBwcm9wVHJ1ZU51bGwoUFZhbHVlKSkpKQp9KSAlPiUgCiAgICBkby5jYWxsKHdoYXQ9cmJpbmQpICU+JSAKICAgIG11dGF0ZShUZXN0PXJvd25hbWVzKC4pKSAlPiUKICAgIHNlbGVjdChUZXN0LCBldmVyeXRoaW5nKCkpCmBgYAoKYGBge3Igc2F2ZV9yZXN1bHRzfQojIFRha2UgYWxsIGdlbmVzIHNpZ25pZmljYW50IGF0IDEwJSBGRFIgb3IgdG9wIDEwMCBnZW5lcywgd2hpY2hldmVyIGlzIG1vcmUKZmlsdGVyZWQucGVhay5yZXN1bHRzLnRhYmxlcyA8LSBsYXBwbHkocGVhay5yZXN1bHRzLnRhYmxlcywgLiAlPiUgZmlsdGVyKEZEUiA8PSAwLjEgfCBzZXFfbGVuKG5yb3coLikpIDw9IDEwMCkpCmRpci5jcmVhdGUoaGVyZSgic2F2ZWRfZGF0YSIsICJDaElQLXNlcSIpLCByZWN1cnNpdmUgPSBUUlVFLCBzaG93V2FybmluZ3MgPSBGQUxTRSkKZGlyLmNyZWF0ZShoZXJlKCJyZXN1bHRzIiwgIkNoSVAtc2VxIiksIHJlY3Vyc2l2ZSA9IFRSVUUsIHNob3dXYXJuaW5ncyA9IEZBTFNFKQpzYXZlUkRTKHdpbmRvdy5yZXN1bHRzLnRhYmxlcywKICAgICAgICBoZXJlKCdzYXZlZF9kYXRhJywgJ0NoSVAtc2VxJywKICAgICAgICAgICAgIGdsdWUoJ3twYXJhbXMkaGlzdG9uZV9tYXJrfS13aW5kb3ctZGlmZm1vZC5SRFMnKSkpCnNhdmVSRFMocGVhay5yZXN1bHRzLnRhYmxlcywKICAgICAgICBoZXJlKCdzYXZlZF9kYXRhJywgJ0NoSVAtc2VxJywKICAgICAgICAgICAgIGdsdWUoJ3twYXJhbXMkaGlzdG9uZV9tYXJrfS1wZWFrLWRpZmZtb2QuUkRTJykpKQpzYXZlLmltYWdlKGhlcmUoJ3NhdmVkX2RhdGEnLCAnQ2hJUC1zZXEnLAogICAgICAgICAgICAgICAgZ2x1ZSgne3BhcmFtcyRoaXN0b25lX21hcmt9LWRpZmZtb2QucmRhJykpKQp3cml0ZS54bHN4KGZpbHRlcmVkLnBlYWsucmVzdWx0cy50YWJsZXMsCiAgICAgICAgICAgaGVyZSgicmVzdWx0cyIsICJDaElQLXNlcSIsCiAgICAgICAgICAgICAgICBnbHVlKCd7cGFyYW1zJGhpc3RvbmVfbWFya30tcGVhay1kaWZmbW9kLnhsc3gnKSkpCmBgYAo=